Index: main/plugin-link-directory/cms-ribbon.xml
===================================================================
--- main/plugin-link-directory/cms-ribbon.xml	(revision 28682)
+++ main/plugin-link-directory/cms-ribbon.xml	(working copy)
@@ -23,25 +23,47 @@
                 <group label="plugin.link-directory:RIBBON_TABS_TAB_LINKDIRECTORY_LINK_ACTIONS_LABEL" icon="">
                     <large/>
                     <medium>
-                        <control id="org.ametys.linkdirectory.Link.add"/>
-                        <control id="org.ametys.linkdirectory.Link.edit"/>
-                       	<control id="org.ametys.linkdirectory.Link.delete"/>
+                        <control id="org.ametys.plugins.linkdirectory.Link.add"/>
+                        <control id="org.ametys.plugins.linkdirectory.Link.edit"/>
+                       	<control id="org.ametys.plugins.linkdirectory.Link.delete"/>
                     </medium>
                     <small>
-                        <control id="org.ametys.linkdirectory.Link.add"/>
+                        <control id="org.ametys.plugins.linkdirectory.Link.add"/>
 						<layout size="very-small">
-                        	<control id="org.ametys.linkdirectory.Link.edit"/>
-                       		<control id="org.ametys.linkdirectory.Link.delete"/>
+                        	<control id="org.ametys.plugins.linkdirectory.Link.edit"/>
+                       		<control id="org.ametys.plugins.linkdirectory.Link.delete"/>
                        	</layout>
                     </small>
                 </group>
                 <group label="plugin.link-directory:RIBBON_TABS_TAB_LINKDIRECTORY_THEMES_LABEL" icon="">
                     <large/>
                     <medium>
-                        <control id="org.ametys.linkdirectory.Themes.manage"/>
+                        <control id="org.ametys.plugins.linkdirectory.Themes.manage"/>
+                    </medium>
+                    <small/>
+                </group>
+                <group label="plugin.link-directory:RIBBON_TABS_TAB_LINKDIRECTORY_ORDER_LABEL" icon="">
+                    <large>
+                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveFirst"/>
+                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveUp"/>
+                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveDown"/>
+                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveLast"/>
+                    </large>
+                    <medium>
+                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveFirst"/>
+                        <layout size="small">
+        	                <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveUp"/>
+    	                    <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveDown"/>
+	                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveLast"/>
+                        </layout>
                     </medium>
                     <small>
-                        <control id="org.ametys.linkdirectory.Themes.manage"/>
+                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveFirst"/>
+                        <layout size="very-small">
+	                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveUp"/>
+	                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveDown"/>
+	                        <control id="org.ametys.plugins.linkdirectory.LinkDirectory.moveLast"/>
+                        </layout>
                     </small>
                 </group>
             </groups>
Index: main/plugin-link-directory/i18n/messages_en.xml
===================================================================
--- main/plugin-link-directory/i18n/messages_en.xml	(revision 28682)
+++ main/plugin-link-directory/i18n/messages_en.xml	(working copy)
@@ -29,6 +29,17 @@
     <message key="RIBBON_TABS_TAB_LINKDIRECTORY_LABEL">Link directory</message>
     <message key="RIBBON_TABS_TAB_LINKDIRECTORY_LINK_ACTIONS_LABEL">Links</message>
     <message key="RIBBON_TABS_TAB_LINKDIRECTORY_THEMES_LABEL">Themes</message>
+    <message key="RIBBON_TABS_TAB_LINKDIRECTORY_ORDER_LABEL">Order of the links</message>
+    
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_FIRST_LABEL">Move first</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_FIRST_DESCRIPTION">Move the link to the top of the list.</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_UP_LABEL">Move up</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_UP_DESCRIPTION">Move the link up one notch.</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_DOWN_LABEL">Move down</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_DOWN_DESCRIPTION">Move the link down one notch.</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_BOTTOM_LABEL">Move last</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_BOTTOM_DESCRIPTION">Move the link to the bottom of the list.</message>
+    
     
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_LABEL">Link directory</message>
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_DESCRIPTION">User interface to manage the directory links.</message>
@@ -48,10 +59,15 @@
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_TITLE">Link text</message>
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_ALTERNATIVE">Link title</message>
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_THEMES">Themes</message>
+    <message key="PLUGINS_LINKDIRECTORY_UITOOL_LIMITED_ACCESS">Limited access</message>
+    <message key="PLUGINS_LINKDIRECTORY_UITOOL_LIMITED_ACCESS_YES">Yes</message>
+    <message key="PLUGINS_LINKDIRECTORY_UITOOL_LIMITED_ACCESS_NO">No</message>
+
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_LOADING">Loading links, please wait...</message>
     
     <message key="PLUGINS_LINKDIRECTORY_GET_LANGUAGES_ERROR">An error occurred while retrieving the available languages for this site.</message>
     <message key="PLUGINS_LINKDIRECTORY_GET_LINK_ERROR">An error occurred while retrieving the link properties.</message>
+    <message key="PLUGINS_LINKDIRECTORY_HANDLE_MOVE_LINK_ERROR">An error occurred while moving the link.</message>
     
     <!-- Dialog -->
     <message key="PLUGINS_LINKDIRECTORY_LINK_ADD_TITLE">Add link</message>
@@ -73,6 +89,12 @@
     <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_THEMES_DESC">Link themes</message>
     <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_CONTENT_LABEL">Description</message>
     <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_CONTENT_DESC">Link description</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANT_ANY_USER_LABEL">Grant any connected user</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANT_ANY_USER_DESC">Check this box if you want every connected user to be able to see this link</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_USERS_LABEL">Restrict access to users</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_USERS_DESC">Select the users who will have access to this link</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_GROUPS_LABEL">Restrict access to groups</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_GROUPS_DESC">Select the groups who will have access to this link</message>
     
     <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_URL_INVALID">You have to fill in a valid URL address.</message>
     
@@ -92,7 +114,8 @@
     <message key="PLUGINS_LINKDIRECTORY_THEME_ADD_TITLE">New theme</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_EDIT_TITLE">Modify theme</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_DELETE_TITLE">Delete theme</message>
-    <message key="PLUGINS_LINKDIRECTORY_THEME_COLUMN_NAME">Name</message>
+    <message key="PLUGINS_LINKDIRECTORY_THEME_COLUMN_NAME">Label</message>
+    <message key="PLUGINS_LINKDIRECTORY_THEME_ID_COLUMN_NAME">Id</message>
     
     <message key="PLUGINS_LINKDIRECTORY_THEME_ADD_LABEL">Enter the new theme name:</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_EDIT_LABEL">Enter the new theme name:</message>
@@ -104,6 +127,10 @@
     <message key="PLUGINS_LINKDIRECTORY_THEME_ERROR_DELETE">An error occurred while deleting the theme.</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_ERROR_ALREADY_EXISTS">This theme already exists.</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_ERROR_UNKNOWN">This theme doesn't exist.</message>
+
+    
+    <message key="PLUGINS_LINKDIRECTORY_EMPTY_THEME_TITLE">Invalid theme name</message>                  
+    <message key="PLUGINS_LINKDIRECTORY_EMPTY_THEME_MSG">The name of the theme cannot be empty</message>
     
     <!-- Themes -->
     <message key="PLUGINS_LINKDIRECTORY_THEME_ADD_ACTION">Add</message>
@@ -122,6 +149,8 @@
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_HEADER_DESC">Service title (optional). Leave empty if you do not want a title.</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_THEMES">Themes</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_THEMES_DESC">Select one or several themes to display only links assigned to these, or leave blank to display all links.</message>
+    <message key="PLUGINS_LINKDIRECTORY_SERVICE_CONFIGURABLE">Customizable</message>
+    <message key="PLUGINS_LINKDIRECTORY_SERVICE_CONFIGURABLE_DESC">Check this box if you want the displayed links to be customizable by the visitor. Bare in mind that this option might slow down the performances.</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_DISPLAY_LINKS">Display urls</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_DISPLAY_LINKS_DESC">Check this box to display the urls of links.</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_PARAM_DISPLAY_THEMES">Display themes</message>
@@ -129,8 +158,28 @@
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_XSLT">Display</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_XSLT_DESC">Select the rendering to use when displaying this service</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_XSLT_DEFAULT10">Default</message>
+    <message key="PLUGINS_LINKDIRECTORY_SERVICE_XSLT_THUMBNAILS">Thumbnails</message>
     
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_DISPLAY_THEMES">Themes: </message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_NO_ENTRY">No link in the directory.</message>
     
+    <message key="PLUGINS_LINKDIRECTORY_DELETED_THEME_TITLE">Deleted themes</message>
+    <message key="PLUGINS_LINKDIRECTORY_DELETED_THEME_TEXT">{amount} deleted theme(s) still referenced by this service. Reconfigure the application to get rid of this error.</message>
+    <message key="PLUGINS_LINK_DIRECTORY_DELETED_THEME_NAMES">The following themes referenced in the file "link-themes.xml" do not exist: </message>
+    
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS">Customize</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS_TITLE">Customize the links to display</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS_TITLE_BO">This service is customizable by every connected user</message>
+    <message key="PLUGINS_LINKDIRECTORY_MAX_LINKS_SELECTED_ALERT">You can't select more than {maxCheckedLinks} links.</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_ERROR">An error has occurred while saving your preferences.</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_TITLE">Check the links you wish to be displayed first. You can select maximum of {maxCheckedLinks} links.</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_CONFIRM">Confirm</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_CANCEL">Cancel</message>
+    <message key="PLUGINS_LINKDIRECTORY_MORE">More</message>
+    <message key="PLUGINS_LINKDIRECTORY_LESS">Less</message>
+    <message key="PLUGINS_LINKDIRECTORY_SEE_MORE">See more links</message>
+    <message key="PLUGINS_LINKDIRECTORY_SEE_LESS">See less links</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURATION_ERROR_TITLE">Configuration error</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURATION_ERROR_TEXT">Consult the application logs for more details on the error.</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURATION_ERROR_IMG_ALT">Error</message>
 </catalogue>
Index: main/plugin-link-directory/i18n/messages_fr.xml
===================================================================
--- main/plugin-link-directory/i18n/messages_fr.xml	(revision 28682)
+++ main/plugin-link-directory/i18n/messages_fr.xml	(working copy)
@@ -29,6 +29,16 @@
     <message key="RIBBON_TABS_TAB_LINKDIRECTORY_LABEL">Annuaire de liens</message>
     <message key="RIBBON_TABS_TAB_LINKDIRECTORY_LINK_ACTIONS_LABEL">Liens</message>
     <message key="RIBBON_TABS_TAB_LINKDIRECTORY_THEMES_LABEL">Thématiques</message>
+    <message key="RIBBON_TABS_TAB_LINKDIRECTORY_ORDER_LABEL">Ordre des liens</message>
+    
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_FIRST_LABEL">Au début</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_FIRST_DESCRIPTION">Place le lien au début de la liste.</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_UP_LABEL">Monter</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_UP_DESCRIPTION">Déplace le lien d'un cran vers le haut.</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_DOWN_LABEL">Descendre</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_DOWN_DESCRIPTION">Déplace le lien d'un cran vers le bas.</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_BOTTOM_LABEL">A la fin</message>
+    <message key="PLUGINS_LINKDIRECTORY_MOVE_BOTTOM_DESCRIPTION">Place le lien à la fin de la liste.</message>
     
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_LABEL">Annuaire de liens</message>
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_DESCRIPTION">Interface de gestion des liens de l'annuaire.</message>
@@ -48,10 +58,14 @@
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_TITLE">Texte du lien</message>
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_ALTERNATIVE">Titre du lien</message>
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_THEMES">Thématiques</message>
+    <message key="PLUGINS_LINKDIRECTORY_UITOOL_LIMITED_ACCESS">Accès limité</message>
+    <message key="PLUGINS_LINKDIRECTORY_UITOOL_LIMITED_ACCESS_YES">Oui</message>
+    <message key="PLUGINS_LINKDIRECTORY_UITOOL_LIMITED_ACCESS_NO">Non</message>
     <message key="PLUGINS_LINKDIRECTORY_UITOOL_LOADING">Chargement des liens, veuillez patienter...</message>
     
     <message key="PLUGINS_LINKDIRECTORY_GET_LANGUAGES_ERROR">Une erreur est survenue : impossible de récupérer la liste des langues disponibles pour ce site.</message>
     <message key="PLUGINS_LINKDIRECTORY_GET_LINK_ERROR">Une erreur est survenue en récupérant les propriétés du lien.</message>
+    <message key="PLUGINS_LINKDIRECTORY_HANDLE_MOVE_LINK_ERROR">Une erreur est survenue en essayant de déplacer le lien.</message>
     
     <!-- Dialog -->
     <message key="PLUGINS_LINKDIRECTORY_LINK_ADD_TITLE">Ajout d'un lien</message>
@@ -73,6 +87,12 @@
     <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_THEMES_DESC">Thématiques du lien</message>
     <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_CONTENT_LABEL">Description</message>
     <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_CONTENT_DESC">Description du lien</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANT_ANY_USER_LABEL">Autoriser tous les utilisateurs connectés</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANT_ANY_USER_DESC">Cocher cette case si vous souhaitez que tous les utilisateurs connectés puissent voir ce lien</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_USERS_LABEL">Limité aux utilisateurs</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_USERS_DESC">Sélectionnez les utilisateurs qui auront accès à ce lien</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_GROUPS_LABEL">Limité aux groupes</message>
+    <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_GROUPS_DESC">Sélectionnez les groupes qui auront accès à ce lien</message>
     
     <message key="PLUGINS_LINKDIRECTORY_LINK_DIALOG_URL_INVALID">Vous devez entrer une adresse URL valide.</message>
     
@@ -92,7 +112,8 @@
     <message key="PLUGINS_LINKDIRECTORY_THEME_ADD_TITLE">Nouvelle thématique</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_EDIT_TITLE">Modifier la thématique</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_DELETE_TITLE">Supprimer une thématique</message>
-    <message key="PLUGINS_LINKDIRECTORY_THEME_COLUMN_NAME">Nom</message>
+    <message key="PLUGINS_LINKDIRECTORY_THEME_COLUMN_NAME">Label</message>
+    <message key="PLUGINS_LINKDIRECTORY_THEME_ID_COLUMN_NAME">Id</message>
     
     <message key="PLUGINS_LINKDIRECTORY_THEME_ADD_LABEL">Entrez le nom de la nouvelle thématique :</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_EDIT_LABEL">Entrez le nouveau nom de la thématique :</message>
@@ -105,6 +126,9 @@
     <message key="PLUGINS_LINKDIRECTORY_THEME_ERROR_ALREADY_EXISTS">Cette thématique existe déjà.</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_ERROR_UNKNOWN">Cette thématique n'existe pas.</message>
     
+    <message key="PLUGINS_LINKDIRECTORY_EMPTY_THEME_TITLE">Nom de thème invalide</message>    
+    <message key="PLUGINS_LINKDIRECTORY_EMPTY_THEME_MSG">Le nom du thème ne peut pas être vide</message>
+
     <!-- Themes -->
     <message key="PLUGINS_LINKDIRECTORY_THEME_ADD_ACTION">Ajouter</message>
     <message key="PLUGINS_LINKDIRECTORY_THEME_EDIT_ACTION">Modifier</message>
@@ -122,6 +146,8 @@
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_HEADER_DESC">Titre du service (optionnel). Laissez vide si vous ne souhaitez pas de titre.</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_THEMES">Thématiques</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_THEMES_DESC">Sélectionnez une ou plusieurs thématiques pour n'afficher que les liens de ces thématiques, ou laissez vide pour afficher tous les liens.</message>
+    <message key="PLUGINS_LINKDIRECTORY_SERVICE_CONFIGURABLE">Personnalisable</message>
+    <message key="PLUGINS_LINKDIRECTORY_SERVICE_CONFIGURABLE_DESC">Cochez cette case si vous souhaitez rendre l'affichage des liens personnalisable par le visiteur. Cette option est cependant susceptible de réduire les performances.</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_DISPLAY_LINKS">Afficher les urls</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_DISPLAY_LINKS_DESC">Cochez cette case pour afficher les urls des liens</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_PARAM_DISPLAY_THEMES">Afficher les thèmes</message>
@@ -129,8 +155,28 @@
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_XSLT">Affichage</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_XSLT_DESC">Rendu à utiliser pour l'affichage du service</message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_XSLT_DEFAULT10">Par défaut</message>
+    <message key="PLUGINS_LINKDIRECTORY_SERVICE_XSLT_THUMBNAILS">Vignettes</message>
     
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_DISPLAY_THEMES">Thématiques : </message>
     <message key="PLUGINS_LINKDIRECTORY_SERVICE_NO_ENTRY">Aucun lien dans l'annuaire.</message>
     
+    <message key="PLUGINS_LINKDIRECTORY_DELETED_THEME_TITLE">Thématiques supprimées</message>
+    <message key="PLUGINS_LINKDIRECTORY_DELETED_THEME_TEXT">Il y a {amount} thématique(s) supprimée(s) toujours référencée(s) par ce service. Reconfigurez le service pour ne plus avoir cette erreur.</message>
+    <message key="PLUGINS_LINK_DIRECTORY_DELETED_THEME_NAMES">Les thématiques suivantes référencées dans le fichier "link-themes.xml" n'existent pas : </message>
+
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS">Personnaliser</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS_TITLE">Personnaliser les liens à afficher</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS_TITLE_BO">Ce service est personnalisable par tout utilisateur connecté</message>
+    <message key="PLUGINS_LINKDIRECTORY_MAX_LINKS_SELECTED_ALERT">Il n'est pas possible de sélectionner plus de {maxCheckedLinks} liens.</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_TITLE">Sélectionnez les liens à afficher en premier. Vous pouvez sélectionner au maximum {maxCheckedLinks} liens.</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_ERROR">Une erreur s'est produit lors de la sauvegarde de vos préférences.</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_CONFIRM">Confirmer</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_CANCEL">Annuler</message>
+    <message key="PLUGINS_LINKDIRECTORY_MORE">Plus</message>
+    <message key="PLUGINS_LINKDIRECTORY_LESS">Moins</message>
+    <message key="PLUGINS_LINKDIRECTORY_SEE_MORE">Voir plus de liens</message>
+    <message key="PLUGINS_LINKDIRECTORY_SEE_LESS">Voir moins de liens</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURATION_ERROR_TITLE">Erreur de configuration</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURATION_ERROR_TEXT">Vous pouvez consulter les logs de l'application pour plus de détails sur l'erreur.</message>
+    <message key="PLUGINS_LINKDIRECTORY_CONFIGURATION_ERROR_IMG_ALT">Erreur</message>
 </catalogue>
Index: main/plugin-link-directory/nodetypes/linkdirectory_nodetypes.xml
===================================================================
--- main/plugin-link-directory/nodetypes/linkdirectory_nodetypes.xml	(revision 28682)
+++ main/plugin-link-directory/nodetypes/linkdirectory_nodetypes.xml	(working copy)
@@ -20,7 +20,7 @@
            xmlns:ametys="http://www.ametys.org/jcr/repository/1.0"
            xmlns:ametys-internal="http://www.ametys.org/jcr/repository/internal/1.0">
     <!-- Links -->
-    <nodeType name="ametys:directoryLinks" isMixin="false" hasOrderableChildNodes="false" primaryItemName="">
+    <nodeType name="ametys:directoryLinks" isMixin="false" hasOrderableChildNodes="true" primaryItemName="">
         <supertypes>
             <supertype>ametys:unstructured</supertype>
         </supertypes>
@@ -43,6 +43,9 @@
         <propertyDefinition name="ametys-internal:picture-id" requiredType="String" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="false"/>
         <propertyDefinition name="ametys-internal:picture-alternative" requiredType="String" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="false"/>
         <propertyDefinition name="ametys-internal:themes" requiredType="String" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="true"/>
+        <propertyDefinition name="ametys-internal:grant-any-user" requiredType="Boolean" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="false"/>
+        <propertyDefinition name="ametys-internal:granted-users" requiredType="String" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="true"/>
+        <propertyDefinition name="ametys-internal:granted-groups" requiredType="String" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="true"/>
         <childNodeDefinition name="ametys-internal:picture" defaultPrimaryType="ametys:binaryMetadata" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" sameNameSiblings="false">
             <requiredPrimaryTypes>
                 <requiredPrimaryType>ametys:binaryMetadata</requiredPrimaryType>
Index: main/plugin-link-directory/pages/services/directory/directory_1.0.xsl
===================================================================
--- main/plugin-link-directory/pages/services/directory/directory_1.0.xsl	(revision 28682)
+++ main/plugin-link-directory/pages/services/directory/directory_1.0.xsl	(working copy)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
-   Copyright 2012 Anyware Services
+   Copyright 2015 Anyware Services
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
@@ -24,7 +24,8 @@
                 extension-element-prefixes="exsl math resolver ametys">
     
     <xsl:import href="service:web://pages/services/common.xsl"/>
-    
+    <xsl:import href="service:link-directory://pages/services/directory/utils/config_utils.xsl"/> 
+
     <xsl:param name="header" select="ametys:serviceParameter('header')"/>
     
     <xsl:variable name="uniqueId" select="substring-after(math:random(), '.')"/>
@@ -43,7 +44,7 @@
     </xsl:variable>
     
     <!-- Used in common.xsl -->
-    <xsl:variable name="common-service-css-class-name">linkdirectory</xsl:variable>
+    <xsl:variable name="common-service-css-class-name">link-directory linkdirectory-default</xsl:variable>
     <xsl:variable name="is-empty" select="not(/links/link)"/> 
     <xsl:variable name="common-service-name-id">service-linkdirectory</xsl:variable>    
     
@@ -54,9 +55,15 @@
     </xsl:template>  
     
     <xsl:template name="common-service-body-nonempty-content-title">
+        <xsl:call-template name="display-configure-links-btn-service">
+            <xsl:with-param name="maxCheckedLinks" select="32"/>
+            <xsl:with-param name="height" select="30"/>
+            <xsl:with-param name="width" select="30"/>
+        </xsl:call-template>
+        
         <xsl:call-template name="common-service-body-nonempty-content-title-content">
             <xsl:with-param name="title" select="$header"/>
-        </xsl:call-template>    
+        </xsl:call-template>
     </xsl:template>
     
     <xsl:template name="common-service-body-empty">
@@ -70,14 +77,14 @@
     </xsl:template>
     
     <xsl:template name="common-service-body-nonempty-content-content">
+        
+        <xsl:call-template name="wrong-or-deleted-themes"/>
+        
         <div id="link-directory-{$uniqueId}">
         	<xsl:attribute name="class">link-directory-body<xsl:if test="/links/link/@pictureType != ''"><xsl:text> pictures</xsl:text></xsl:if></xsl:attribute>
             <ul class="links">
-                <xsl:apply-templates select="/links/link[normalize-space(@title) != '']">
-                    <xsl:sort select="@title"/>
-                </xsl:apply-templates>
-                <xsl:apply-templates select="/links/link[normalize-space(@title) = '']">
-                    <xsl:sort select="@url"/>
+                <xsl:apply-templates select="/links/link">
+                    <xsl:sort select="@user-selected='true'" order="descending"/>
                 </xsl:apply-templates>
             </ul>
         </div>
@@ -116,14 +123,12 @@
                         </xsl:when>
                     </xsl:choose>
                 </xsl:variable>
-                <a href="{@url}">
+                <a href="{@url}" onclick="window.open(this.href); return false;" >
                     <xsl:if test="normalize-space(@title) != ''">
                         <xsl:attribute name="title"><xsl:value-of select="@title"/></xsl:attribute>
                     </xsl:if>
                     <img src="{resolver:resolveBoundedImage(@imageType, $id, 100, 100)}">
-                        <xsl:if test="@pictureAlternative">
-                            <xsl:attribute name="alt"><xsl:value-of select="@pictureAlternative"/></xsl:attribute>
-                        </xsl:if>
+                        <xsl:attribute name="alt"><xsl:value-of select="@pictureAlternative"/></xsl:attribute>
                     </img>
                 </a>
 	        </div>
@@ -179,5 +184,4 @@
        	</xsl:if>
     </xsl:template>
     
-    
 </xsl:stylesheet>
Index: main/plugin-link-directory/pages/services/directory/thumbnails_1.1.xml
===================================================================
--- main/plugin-link-directory/pages/services/directory/thumbnails_1.1.xml	(revision 0)
+++ main/plugin-link-directory/pages/services/directory/thumbnails_1.1.xml	(revision 0)
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 2014 Anyware Services
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+   -->
+<service>
+    <label i18n="true">PLUGINS_LINKDIRECTORY_SERVICE_XSLT_THUMBNAILS</label>
+</service>
Index: main/plugin-link-directory/pages/services/directory/thumbnails_1.1.xsl
===================================================================
--- main/plugin-link-directory/pages/services/directory/thumbnails_1.1.xsl	(revision 0)
+++ main/plugin-link-directory/pages/services/directory/thumbnails_1.1.xsl	(revision 0)
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 2015 Anyware Services
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+   -->
+<xsl:stylesheet version="1.0" 
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:i18n="http://apache.org/cocoon/i18n/2.1"
+                xmlns:math="java.lang.Math"
+                xmlns:resolver="org.ametys.cms.transformation.xslt.ResolveURIComponent"
+                xmlns:ametys="org.ametys.web.transformation.xslt.AmetysXSLTHelper"
+                extension-element-prefixes="math resolver ametys">
+    
+    <xsl:import href="service:web://pages/services/common.xsl"/>
+    <xsl:import href="service:link-directory://pages/services/directory/utils/config_utils.xsl"/> 
+    <xsl:import href="service:link-directory://pages/services/directory/utils/thumbnails_utils.xsl"/> 
+    
+    <xsl:param name="header" select="ametys:serviceParameter('header')"/>
+    
+    <xsl:variable name="uniqueId" select="substring-after(math:random(), '.')"/>
+    
+    <!-- Max number of links displayed before a "See more" button appears -->    
+    <xsl:variable name="moreThreshold" select="8"/>
+        
+    <xsl:variable name="raw-is-title-visible">        
+        <xsl:call-template name="common-service-head-title-content">
+            <xsl:with-param name="title" select="$header"/>                    
+        </xsl:call-template>
+    </xsl:variable>    
+    <xsl:variable name="is-title-visible" select="string-length($raw-is-title-visible) &gt; 0"/>
+    <xsl:variable name="level">
+        <xsl:choose>
+            <xsl:when test="$is-title-visible">2</xsl:when>
+            <xsl:otherwise>1</xsl:otherwise>
+        </xsl:choose>
+    </xsl:variable>
+    
+    <!-- Used in common.xsl -->
+    <xsl:variable name="common-service-css-class-name">link-directory linkdirectory-thumbnails</xsl:variable>
+    <xsl:variable name="is-empty" select="not(/links/link)"/> 
+    <xsl:variable name="common-service-name-id">service-linkdirectory-thumbnails</xsl:variable>    
+    
+    <xsl:template name="common-service-head-title">
+        <xsl:call-template name="common-service-head-title-content">
+            <xsl:with-param name="title" select="$header"/>                  
+        </xsl:call-template>
+    </xsl:template>  
+    
+    <xsl:template name="common-service-body-nonempty-content-title">
+        
+        <!-- Display the configure links button if the service is configurable -->
+        <xsl:call-template name="display-configure-links-btn-service">
+	        <xsl:with-param name="maxCheckedLinks" select="$moreThreshold - 1"/>
+            <xsl:with-param name="height" select="30"/>
+            <xsl:with-param name="width" select="30"/>
+        </xsl:call-template>
+        
+        <xsl:call-template name="common-service-body-nonempty-content-title-content">
+            <xsl:with-param name="title" select="$header"/>
+        </xsl:call-template>    
+
+    </xsl:template>
+    
+    <xsl:template name="common-service-body-empty">
+        <div class="ametys-cms-service {$common-service-css-class-name} wrapper">
+            <xsl:call-template name="common-service-body-nonempty-content-title"/>
+            <p>
+                <i18n:text i18n:key="PLUGINS_LINKDIRECTORY_SERVICE_NO_ENTRY"/>
+            </p>
+        </div>      
+    </xsl:template>
+    
+    <xsl:template name="common-service-body-nonempty-content-content">
+        
+        <!-- Display the errors if any -->
+        <xsl:call-template name="wrong-or-deleted-themes"/>   
+        
+        <!-- Display the links in thumbnails mode -->
+        <xsl:call-template name="links-thumbnails">
+            <xsl:with-param name="links" select="/links/link"/>
+            <xsl:with-param name="moreThreshold" select="$moreThreshold"/>
+            <xsl:with-param name="height" select="100"/>
+            <xsl:with-param name="width" select="100"/>
+            <xsl:with-param name="showThemes" select="ametys:serviceParameter('showThemes')"/>
+            <xsl:with-param name="showUrl" select="ametys:serviceParameter('showUrl')"/>
+        </xsl:call-template>
+           
+    </xsl:template>
+    
+</xsl:stylesheet>
Index: main/plugin-link-directory/pages/services/directory/utils/config_utils.xsl
===================================================================
--- main/plugin-link-directory/pages/services/directory/utils/config_utils.xsl	(revision 0)
+++ main/plugin-link-directory/pages/services/directory/utils/config_utils.xsl	(revision 0)
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 2015 Anyware Services
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+   -->
+<xsl:stylesheet version="1.0" 
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:ametys="org.ametys.web.transformation.xslt.AmetysXSLTHelper"
+                xmlns:math="java.lang.Math"
+                xmlns:i18n="http://apache.org/cocoon/i18n/2.1">
+    
+    <!-- Display the configure links button if the service is configurable -->
+    <xsl:template name="display-configure-links-btn-service">    
+        <xsl:param name="height"/>
+        <xsl:param name="width"/>
+        <xsl:param name="maxCheckedLinks"/>
+        
+        <xsl:if test="ametys:serviceParameter('configurable') = 'true'">
+	        <xsl:call-template name="configure-links-btn-and-dialog">
+	            <xsl:with-param name="links" select="/links/link"/>
+	            <xsl:with-param name="height" select="$height"/>
+	            <xsl:with-param name="width" select="$width"/>
+	            <xsl:with-param name="maxCheckedLinks" select="$maxCheckedLinks"/>
+	        </xsl:call-template>                                     
+        </xsl:if>
+    </xsl:template>
+    
+    <!-- Configure links button for a service or an input data -->
+    <xsl:template name="configure-links-btn-and-dialog">
+        <xsl:param name="links"/>
+        <xsl:param name="height" select="32"/>
+        <xsl:param name="width" select="32"/>
+        <xsl:param name="maxCheckedLinks"/>
+        
+        <xsl:variable name="buttonId" select="substring-after(math:random(), '.')"/>
+        
+        <script type="text/javascript">
+            
+            <!-- Rendering of the button -->    
+            <xsl:call-template name="configure-links-btn">
+                <xsl:with-param name="height" select="$height"/>
+                <xsl:with-param name="width" select="$width"/>
+                <xsl:with-param name="buttonId" select="$buttonId"/>
+            </xsl:call-template>
+            
+            <!-- Set up the dialog's form -->
+            <xsl:call-template name="configure-links-dialog-and-form">
+                <xsl:with-param name="maxCheckedLinks" select="$maxCheckedLinks"/>
+                <xsl:with-param name="links" select="$links"/>
+                <xsl:with-param name="buttonId" select="$buttonId"/>
+            </xsl:call-template> 
+             
+             <!-- Logic of the button and its dialog -->
+             <xsl:call-template name="configure-links-btn-script">
+                 <xsl:with-param name="maxCheckedLinks" select="$maxCheckedLinks"/>
+                 <xsl:with-param name="buttonId" select="$buttonId"/>
+             </xsl:call-template>
+        
+        </script>
+    </xsl:template>
+    
+    <!-- Default rendering for a service -->
+    <xsl:template name="configure-links-btn">
+        <xsl:param name="height"/>
+        <xsl:param name="width"/>
+        <xsl:param name="buttonId"/>
+        
+        <xsl:choose>
+            <xsl:when test="$rendering-context = 'front' and not(ametys:user())">
+                <!-- The user is not connected: the button is not displayed -->
+            </xsl:when>
+            <xsl:when test="$rendering-context != 'front'">
+                document.writeln("&lt;a id=\"configure-links-hyperlink-<xsl:value-of select="$buttonId"/>-disabled\" class=\"configure-links-service-button\" title=\"<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS_TITLE_BO" i18n:catalogue="plugin.link-directory"/>\"&gt;<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS" i18n:catalogue="plugin.link-directory"/>&lt;/a&gt;");
+            </xsl:when>
+            <xsl:otherwise>
+                document.writeln("&lt;a id=\"configure-links-hyperlink-<xsl:value-of select="$buttonId"/>\" class=\"configure-links-service-button\" href=\"javascript:void(0)\" title=\"<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS_TITLE" i18n:catalogue="plugin.link-directory"/>\" i18n:attr=\"title\"&gt;<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS" i18n:catalogue="plugin.link-directory"/>&lt;/a&gt;");
+            </xsl:otherwise>
+        </xsl:choose>
+    </xsl:template>
+    
+    <!-- The dialog and the form when clicking the button -->
+    <xsl:template name="configure-links-dialog-and-form"> 
+        <xsl:param name="maxCheckedLinks"/>
+        <xsl:param name="links"/>
+        <xsl:param name="buttonId"/>
+        
+        document.writeln("&lt;div id=\"configure-links-dialog-<xsl:value-of select="$buttonId"/>\" title=\"<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS_TITLE" i18n:catalogue="plugin.link-directory"/>\" style=\"display:none;\" i18n:attr=\"title\"&gt;");
+           document.writeln("&lt;form id=\"configure-links-form-<xsl:value-of select="$buttonId"/>\"&gt;");
+               document.writeln("&lt;p class=\"configure-links-dialog-text\"&gt;<i18n:translate><i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_TITLE" i18n:catalogue="plugin.link-directory"/><i18n:param name="maxCheckedLinks"><xsl:value-of select="$maxCheckedLinks"/></i18n:param></i18n:translate>&lt;/p&gt;")               
+               document.writeln("&lt;input type=\"hidden\" id=\"zone-item-id-<xsl:value-of select="$buttonId"/>\" name=\"zone-item-id\" value=\"<xsl:value-of select="@zoneItemId"/>\"&gt;");
+
+               <xsl:for-each select="$links">
+                     <xsl:apply-templates select="." mode="configure-field"/>
+               </xsl:for-each>
+               
+           document.writeln("&lt;/form&gt;");
+        document.writeln("&lt;/div&gt;");
+    </xsl:template>
+    
+    <!-- Configure the checkboxes of the form -->    
+    <xsl:template match="link" mode="configure-field">
+        <xsl:variable name="label">
+            <xsl:choose>
+                <xsl:when test="normalize-space(@title) != ''">
+                    <xsl:value-of select="@title"/>
+                </xsl:when>
+                <xsl:otherwise>
+                    <xsl:value-of select="@url"/>
+                </xsl:otherwise>
+            </xsl:choose>
+        </xsl:variable>
+        
+        document.writeln("&lt;div class=\"field\"&gt;");
+            document.writeln("&lt;input type=\"checkbox\" <xsl:value-of select="@user-selected = 'true'"/> data-serverid=\"<xsl:value-of select="@id"/>\"/&gt;");
+            document.writeln("&lt;label for=\"label-<xsl:value-of select="$label"/>\"&gt;");
+                document.writeln("&lt;span&gt;<xsl:value-of select="$label"/>&lt;/span&gt;");
+            document.writeln("&lt;/label&gt;");
+            document.writeln("&lt;div class=\"clearfix\"&gt;&lt;/div&gt;");
+        document.writeln("&lt;/div&gt;");
+    </xsl:template>
+    
+    <!--  Script associated to the configure links button -->
+    <xsl:template name="configure-links-btn-script">
+        <xsl:param name="maxCheckedLinks"/>
+        <xsl:param name="buttonId"/>
+        
+        function saveUserPrefs<xsl:value-of select="$buttonId"/>()
+        {   
+            if ($j('#configure-links-form-<xsl:value-of select="$buttonId"/> input[type="checkbox"]:checked').length &gt; <xsl:value-of select="$maxCheckedLinks"/>)
+            {
+                alert("<i18n:translate>
+                        <i18n:text i18n:key='PLUGINS_LINKDIRECTORY_MAX_LINKS_SELECTED_ALERT' i18n:catalogue='plugin.link-directory'/>
+                        <i18n:param name="maxCheckedLinks"><xsl:value-of select="$maxCheckedLinks"/></i18n:param>
+                    </i18n:translate>");
+                return;
+            }
+        
+            // Generate form data
+            var data = {
+                'zone-item-id': $j('#zone-item-id-<xsl:value-of select="$buttonId"/>').val()
+            };
+            
+            $j('#configure-links-form-<xsl:value-of select="$buttonId"/> input[type="checkbox"]').each(function() {
+                var box = $j(this);
+                data['checkbox-' + encodeURIComponent(box.attr('data-serverid'))] = box.is(':checked');
+            });
+            
+            $j.ajax({
+                url: '<xsl:value-of select="$site-uri-prefix"/>/_plugins/link-directory/set-user-preferences?language=<xsl:value-of select="ametys:lang()"/>',
+                data: data,
+                success: function(data) { window.location.reload(); },
+                error: function(data) {alert("<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_ERROR" i18n:catalogue="plugin.link-directory"/>");}
+            });
+        }
+        
+        $j('#configure-links-hyperlink-<xsl:value-of select="$buttonId"/>').on('click', function() {
+            $j('#configure-links-dialog-<xsl:value-of select="$buttonId"/>').dialog('open');
+        });
+        
+        $j('#configure-links-dialog-<xsl:value-of select="$buttonId"/>').dialog({
+            autoOpen: false,
+            resizable: false,
+            maxHeight: 435,
+            width: 400,
+            modal: true,
+            buttons: {
+                "<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_CONFIRM" i18n:catalogue="plugin.link-directory"/>": function() {
+                    saveUserPrefs<xsl:value-of select="$buttonId"/>();
+                    $j(this).dialog('close');
+                },
+                "<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_DIALOG_CANCEL" i18n:catalogue="plugin.link-directory"/>": function() {
+                    $j(this).dialog('close');
+                }
+            }
+        });
+    </xsl:template>
+    
+     <!--  Rendering of the errors -->
+    <xsl:template name="wrong-or-deleted-themes">
+        <xsl:call-template name="wrong-or-deleted-themes-impl">
+            <xsl:with-param name="data" select="/links/deleted-themes"/>
+        </xsl:call-template>
+    </xsl:template>
+    
+         <!--  Rendering of the errors -->
+    <xsl:template name="wrong-or-deleted-themes-impl">
+        <xsl:param name="data"/>
+        
+        <xsl:if test="$data">
+            <xsl:if test="$rendering-context = 'back'">
+                <div class="ametys-cms-zone-error">
+                   <div class="ametys-cms-zone-item-error">
+                       <div class="ametys-cms-content-inline-error">
+                           <table cellspacing="0" cellpadding="0">
+                                <tbody>
+                                    <tr>
+                                        <td style="vertical-align: top; width: 36px;">
+                                            <img alt="" src="{$uri-prefix}/plugins/web/resources/img/skin/zoneitem_error_32.png"/>
+                                        </td>
+                                        <td>
+                                            <strong><i18n:text i18n:key="PLUGINS_LINKDIRECTORY_DELETED_THEME_TITLE" i18n:catalogue="plugin.link-directory"/></strong>
+                                            <br/>
+                                            <em>
+                                                <i18n:translate>
+                                                    <i18n:text i18n:key="PLUGINS_LINKDIRECTORY_DELETED_THEME_TEXT" i18n:catalogue="plugin.link-directory"/>
+                                                    <i18n:param name="amount"><xsl:value-of select="$data"/></i18n:param>
+                                                </i18n:translate>
+                                            </em>
+                                        </td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </xsl:if>
+        </xsl:if>
+    </xsl:template>
+    
+</xsl:stylesheet>
+    
\ No newline at end of file
Index: main/plugin-link-directory/pages/services/directory/utils/inputdata_utils.xsl
===================================================================
--- main/plugin-link-directory/pages/services/directory/utils/inputdata_utils.xsl	(revision 0)
+++ main/plugin-link-directory/pages/services/directory/utils/inputdata_utils.xsl	(revision 0)
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 2015 Anyware Services
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+   -->
+<xsl:stylesheet version="1.0" 
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:ametys="org.ametys.web.transformation.xslt.AmetysXSLTHelper"
+                xmlns:resolver="org.ametys.cms.transformation.xslt.ResolveURIComponent"
+                xmlns:i18n="http://apache.org/cocoon/i18n/2.1"
+                xmlns:exsl="http://exslt.org/common">
+                
+    <xsl:import href="service:link-directory://pages/services/directory/utils/config_utils.xsl"/>            
+    <xsl:import href="service:link-directory://pages/services/directory/utils/thumbnails_utils.xsl"/>
+    
+    <!-- Display the configure links button if the inputdata is configurable -->
+    <xsl:template name="display-configure-links-btn-inputdata">    
+        <xsl:param name="height"/>
+        <xsl:param name="width"/>
+        <xsl:param name="maxCheckedLinks"/>
+        
+        <xsl:if test="/cms/inputData/linkDirectory/@configurable = 'true'">
+            <xsl:call-template name="configure-links-btn-and-dialog">
+                <xsl:with-param name="links" select="/cms/inputData/linkDirectory/links/link"/>
+                <xsl:with-param name="height" select="$height"/>
+                <xsl:with-param name="width" select="$width"/>
+                <xsl:with-param name="maxCheckedLinks" select="$maxCheckedLinks"/>
+            </xsl:call-template>                                     
+        </xsl:if>
+    </xsl:template>
+    
+    <!-- Input data configure links button : default rendering -->
+    <xsl:template name="configure-links-btn">
+        <xsl:param name="height"/>
+        <xsl:param name="width"/>
+        <xsl:param name="buttonId"/>
+        
+        <xsl:choose>
+            <xsl:when test="$rendering-context != 'front'">
+                <!-- Default rendering for a configure links button for an input data in BO/preview/live : no rendering -->
+            </xsl:when>
+            <xsl:otherwise>
+                <xsl:choose>
+                   <xsl:when test="ametys:user()">
+                       document.writeln("&lt;a id=\"configure-links-hyperlink-<xsl:value-of select="$buttonId"/>\" class=\"configure-links-inputdata-button\" href=\"javascript:void(0)\" title=\"<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS_TITLE" i18n:catalogue="plugin.link-directory"/>\"&gt;<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURE_THUMBNAILS" i18n:catalogue="plugin.link-directory"/>&lt;/a&gt;");
+                   </xsl:when>
+                   <xsl:otherwise>
+                       <!-- The user is not connected : no rendering -->
+                   </xsl:otherwise> 
+                </xsl:choose>
+            </xsl:otherwise>
+        </xsl:choose>
+    </xsl:template>
+    
+    <xsl:template name="links-thumbnails-inputdata">
+        <xsl:param name="moreThreshold"/>
+        <xsl:param name="height"/>
+        <xsl:param name="width"/>
+        <xsl:param name="showThemes" select="'false'"/>
+        <xsl:param name="showUrl" select="'false'"/>
+    
+        <xsl:call-template name="links-thumbnails">
+            <xsl:with-param name="links" select="/cms/inputData/linkDirectory/links/link"/>
+            <xsl:with-param name="moreThreshold" select="$moreThreshold"/>
+            <xsl:with-param name="height" select="$height"/>
+            <xsl:with-param name="width" select="$width"/>
+            <xsl:with-param name="showThemes" select="$showThemes"/>
+            <xsl:with-param name="showUrl" select="$showUrl"/>
+        </xsl:call-template>
+    </xsl:template>
+    
+    <!-- Template used when "link-themes.xml" is invalid -->    
+    <xsl:template name="linkdirectory-configuration-error">    
+        <xsl:if test="/cms/inputData/linkDirectory/@error">
+	        <xsl:if test="$rendering-context = 'back'">
+	            <div class="ametys-cms-zone ametys-cms-zone-error">
+	                <img style="float: left" alt="" src="{$uri-prefix}/plugins/web/resources/img/skin/zone_error_32.png"/>
+	                <strong><i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURATION_ERROR_TITLE" i18n:catalogue="plugin.link-directory"/></strong>
+	                <br/>
+	                <div class="ametys-cms-zone-error-desc">
+	                   <i18n:text i18n:key="PLUGINS_LINKDIRECTORY_CONFIGURATION_ERROR_TEXT" i18n:catalogue="plugin.link-directory"/>
+	                <xsl:value-of select="/cms/inputData/linkDirectory/@error"/>
+                    </div>
+	            </div>
+	        </xsl:if>
+        </xsl:if>
+    </xsl:template>
+    
+    <!-- Handle wrong/deleted themes and configuration errors -->
+    <xsl:template name="wrong-or-deleted-themes">
+        <xsl:call-template name="wrong-or-deleted-themes-impl">
+            <xsl:with-param name="data" select="/cms/inputData/linkDirectory/links/wrong-themes"/>
+        </xsl:call-template>
+    </xsl:template>
+    
+</xsl:stylesheet>
\ No newline at end of file
Index: main/plugin-link-directory/pages/services/directory/utils/thumbnails_utils.xsl
===================================================================
--- main/plugin-link-directory/pages/services/directory/utils/thumbnails_utils.xsl	(revision 0)
+++ main/plugin-link-directory/pages/services/directory/utils/thumbnails_utils.xsl	(revision 0)
@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright 2015 Anyware Services
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+   -->
+<xsl:stylesheet version="1.0" 
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+                xmlns:ametys="org.ametys.web.transformation.xslt.AmetysXSLTHelper"
+                xmlns:resolver="org.ametys.cms.transformation.xslt.ResolveURIComponent"
+                xmlns:exsl="http://exslt.org/common"
+                xmlns:i18n="http://apache.org/cocoon/i18n/2.1">
+    
+    <!--  The links in thumbnails mode -->
+    <xsl:template name="links-thumbnails">
+        <xsl:param name="links"/>
+        <xsl:param name="moreThreshold"/>
+        <xsl:param name="height" select="96"/>
+        <xsl:param name="width" select="96"/>
+        <xsl:param name="showThemes"/>
+        <xsl:param name="showUrl"/>
+        
+        <!-- Display an error if the selected themes are invalid -->
+        <xsl:call-template name="wrong-or-deleted-themes"/>
+
+        <xsl:variable name="links-raw">
+            <xsl:call-template name="separate-links">
+                <xsl:with-param name="links" select="$links"/>
+                <xsl:with-param name="moreThreshold" select="$moreThreshold"/>
+            </xsl:call-template>
+        </xsl:variable>
+        
+        <xsl:variable name="separated-links" select="exsl:node-set($links-raw)"/>
+        
+        <!-- The first $moreThreshold links -->
+        <ul class="links">
+            <xsl:apply-templates select="$separated-links/primary/link" mode="thumbnails">
+                <xsl:with-param name="height" select="$height"/>
+                <xsl:with-param name="width" select="$width"/>
+                <xsl:with-param name="showThemes" select="$showThemes"/>
+                <xsl:with-param name="showUrl" select="$showUrl"/>
+            </xsl:apply-templates>     
+            
+            <xsl:if test="count($links) &gt; $moreThreshold">
+                <xsl:call-template name="see-more-less">
+                    <xsl:with-param name="height" select="$height"/>
+                    <xsl:with-param name="width" select="$width"/>
+                    <xsl:with-param name="buttonId" select="$uniqueId"/>
+                </xsl:call-template>
+            </xsl:if>
+        </ul>
+            
+        <!-- The remaining links -->
+        <xsl:if test="$separated-links/secondary">
+            <ul class="links" id="secondary-links-{$uniqueId}">
+	            <xsl:apply-templates select="$separated-links/secondary/link" mode="thumbnails">
+	               <xsl:with-param name="height" select="$height"/>
+                   <xsl:with-param name="width" select="$width"/>
+                   <xsl:with-param name="showThemes" select="$showThemes"/>
+                   <xsl:with-param name="showUrl" select="$showUrl"/>
+                </xsl:apply-templates>
+            </ul>
+            <script type="text/javascript">
+                $j("#secondary-links-<xsl:value-of select="$uniqueId"/>").hide();
+            </script>      
+        </xsl:if>
+    </xsl:template>
+    
+    <!-- Separate the links in two groups for input data and service in thumbnails view -->
+    <xsl:template name="separate-links">
+        <xsl:param name="links"/>
+        <xsl:param name="moreThreshold"/>
+        
+        <primary>
+            <xsl:for-each select="$links">
+                <xsl:sort select="@user-selected = 'true'" order="descending"/>
+                
+                <xsl:if test="position() &lt; $moreThreshold or (count($links) = 8 and position() = $moreThreshold)">
+                    <xsl:copy-of select="."/>
+                </xsl:if>
+            </xsl:for-each>
+        </primary>
+        <secondary>
+            <xsl:for-each select="$links">
+                <xsl:sort select="@user-selected = 'true'" order="descending"/>
+                
+                <xsl:choose>
+                    <xsl:when test="position() &gt;= $moreThreshold">
+                        <xsl:copy-of select="."/>
+                    </xsl:when>
+                </xsl:choose>
+            </xsl:for-each>
+        </secondary>
+    </xsl:template>
+    
+    <xsl:template match="link" mode="thumbnails">
+        <xsl:param name="height"/>
+        <xsl:param name="width"/>
+        <xsl:param name="showThemes"/>
+        <xsl:param name="showUrl"/>
+        
+        <li>
+            <xsl:attribute name="class">link <xsl:call-template name="common-utils-class"/></xsl:attribute>
+            
+            <a href="{@url}" onclick="window.open(this.href); return false;">
+                 <xsl:call-template name="link-title">
+                    <xsl:with-param name="showThemes" select="$showThemes"/>
+                    <xsl:with-param name="showUrl" select="$showUrl"/>
+                 </xsl:call-template>
+                 <xsl:call-template name="link-picture">
+                    <xsl:with-param name="height" select="$height"/>
+                    <xsl:with-param name="width" select="$width"/>
+                 </xsl:call-template>
+                 <xsl:call-template name="link-text"/>
+            </a>
+        </li>
+    </xsl:template>
+    
+    <xsl:template name="link-title">
+        <xsl:param name="showThemes"/>
+        <xsl:param name="showUrl"/>
+        
+        <xsl:attribute name="title">
+           <xsl:choose>
+               <xsl:when test="normalize-space(@alternative) != ''">
+                   <xsl:value-of select="@alternative"/>
+               </xsl:when>
+               <xsl:otherwise>
+                   <xsl:value-of select="@url"/>
+               </xsl:otherwise>
+           </xsl:choose>
+           <xsl:if test="$showThemes = 'true'">
+                <xsl:for-each select="themes/theme">
+                    <xsl:if test="position() = 1">
+                         <xsl:text> - </xsl:text>
+                    </xsl:if>
+                    <xsl:value-of select="@label"/>
+                    <xsl:if test="position() != last()">
+                         <xsl:text>, </xsl:text>
+                    </xsl:if>
+                </xsl:for-each>
+           </xsl:if>
+           <xsl:if test="$showUrl = 'true'">
+               <xsl:text> (</xsl:text><xsl:value-of select="@url"/><xsl:text>)</xsl:text>
+           </xsl:if>
+        </xsl:attribute>
+    </xsl:template>
+    
+    <xsl:template name="link-picture">
+        <xsl:param name="height"/>
+        <xsl:param name="width"/>
+        
+        <xsl:if test="@pictureType != ''">
+            <xsl:variable name="id">
+                <xsl:choose>
+                    <xsl:when test="@pictureType = 'resource'">
+                        <xsl:value-of select="@pictureId"/>
+                    </xsl:when>
+                    <xsl:when test="@pictureType = 'external'">
+                        <xsl:value-of select="concat(@picturePath, '?objectId=', @id)"/>
+                    </xsl:when>
+                </xsl:choose>
+            </xsl:variable>
+            
+            <span class="picture">
+                <img src="{resolver:resolveBoundedImage(@imageType, $id, $height, $width)}" alt="{@pictureAlternative}"/>
+            </span>
+         </xsl:if>
+    </xsl:template>
+                
+    <xsl:template name="link-text">
+        <span>
+            <xsl:attribute name="class">text <xsl:if test="@pictureType = ''">nopicture</xsl:if></xsl:attribute>
+        
+             <xsl:choose>
+                 <xsl:when test="@title != ''"> 
+                        <xsl:value-of select="@title"/>
+                 </xsl:when>
+                 <xsl:otherwise>
+                        <xsl:value-of select="@url"/>
+                 </xsl:otherwise>
+             </xsl:choose>
+        </span>
+    </xsl:template>
+    
+    <!-- The see more/less links functionality -->
+    <xsl:template name="see-more-less">
+        <xsl:param name="height"/>
+        <xsl:param name="width"/>
+        <xsl:param name="buttonId"/>
+        
+        <script type="text/javascript">
+            
+            document.write("&lt;li id=\"show-more-links-<xsl:value-of select="$buttonId"/>\" class=\"link\" &gt;&lt;a href=\"javascript:showSecondaryLinks<xsl:value-of select="$buttonId"/>()\" title=\"<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_SEE_MORE" i18n:catalogue="plugin.link-directory"/>\"&gt;");
+            <xsl:call-template name="show-more-picture">
+                <xsl:with-param name="height" select="$height"/>
+                <xsl:with-param name="width" select="$width"/>
+            </xsl:call-template>
+            document.write("&lt;/a&gt;&lt;/li&gt;");
+            
+            document.write("&lt;li id=\"show-less-links-<xsl:value-of select="$buttonId"/>\" class=\"link\" &gt;&lt;a href=\"javascript:hideSecondaryLinks<xsl:value-of select="$buttonId"/>()\" title=\"<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_SEE_LESS" i18n:catalogue="plugin.link-directory"/>\"&gt;");
+            <xsl:call-template name="show-less-picture">
+                <xsl:with-param name="height" select="$height"/>
+                <xsl:with-param name="width" select="$width"/>
+            </xsl:call-template>
+            document.write("&lt;/a&gt;&lt;/li&gt;");
+            
+            function showSecondaryLinks<xsl:value-of select="$buttonId"/>()
+            {
+                $j("#show-more-links-<xsl:value-of select="$buttonId"/>").hide();
+                $j("#show-less-links-<xsl:value-of select="$buttonId"/>").show();
+                $j("#secondary-links-<xsl:value-of select="$buttonId"/>").show();
+            }
+            
+            function hideSecondaryLinks<xsl:value-of select="$buttonId"/>()
+            {
+                $j("#show-more-links-<xsl:value-of select="$buttonId"/>").show();
+                $j("#show-less-links-<xsl:value-of select="$buttonId"/>").hide();
+                $j("#secondary-links-<xsl:value-of select="$buttonId"/>").hide();
+            }
+    
+            $j("#show-less-links-<xsl:value-of select="$buttonId"/>").hide();
+        </script>
+    </xsl:template>
+   
+    <xsl:template name="show-less-picture">
+        document.write("&lt;span class=\"picture\"&gt;<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_LESS" i18n:catalogue="plugin.link-directory"/>&lt;/span&gt;");
+    </xsl:template>
+   
+    <xsl:template name="show-more-picture">
+        document.write("&lt;span class=\"picture\"&gt;<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_MORE" i18n:catalogue="plugin.link-directory"/>&lt;/span&gt;");
+    </xsl:template>
+    
+</xsl:stylesheet>
+    
\ No newline at end of file
Index: main/plugin-link-directory/plugin.xml
===================================================================
--- main/plugin-link-directory/plugin.xml	(revision 28682)
+++ main/plugin-link-directory/plugin.xml	(working copy)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <!--
-   Copyright 2012 Anyware Services
+   Copyright 2015 Anyware Services
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
@@ -143,7 +143,7 @@
                 </scripts>      
             </extension> 
             
-            <extension id="org.ametys.linkdirectory.Link.add" 
+            <extension id="org.ametys.plugins.linkdirectory.Link.add" 
                        point="org.ametys.cms.workspace.ribbon.RibbonControlsManager" 
                        class="org.ametys.runtime.ui.impl.StaticContextualClientSideElement">
                 <action class="org.ametys.ribbon.control.button.ActionButton">
@@ -163,12 +163,13 @@
                 <right>LinkDirectory_Rights_Links_Handle</right>
             </extension>
         
-            <extension id="org.ametys.linkdirectory.Link.edit"
+            <extension id="org.ametys.plugins.linkdirectory.Link.edit"
                        point="org.ametys.cms.workspace.ribbon.RibbonControlsManager" 
                        class="org.ametys.runtime.ui.impl.StaticContextualClientSideElement">
                 <action class="org.ametys.ribbon.control.button.ActionSelectionButton">
                     <param name="action">org.ametys.linkdirectory.Edit.editLink</param>
                     <param name="target-type">^link-directory$</param>
+                    <param name="enable-multiselection">false</param>
                     
                     <param name="label" i18n="true">PLUGINS_LINKDIRECTORY_LINK_EDIT_LABEL</param>
                     <param name="default-description" i18n="true">PLUGINS_LINKDIRECTORY_LINK_EDIT_DESCRIPTION</param>
@@ -184,7 +185,7 @@
                 <right>LinkDirectory_Rights_Links_Handle</right>
             </extension>
             
-            <extension id="org.ametys.linkdirectory.Link.delete"
+            <extension id="org.ametys.plugins.linkdirectory.Link.delete"
                        point="org.ametys.cms.workspace.ribbon.RibbonControlsManager" 
                        class="org.ametys.runtime.ui.impl.StaticContextualClientSideElement">
                 <action class="org.ametys.ribbon.control.button.ActionSelectionButton">
@@ -205,7 +206,7 @@
                 <right>LinkDirectory_Rights_Links_Handle</right>
             </extension>
             
-            <extension id="org.ametys.linkdirectory.Themes.manage" 
+            <extension id="org.ametys.plugins.linkdirectory.Themes.manage" 
                        point="org.ametys.cms.workspace.ribbon.RibbonControlsManager" 
                        class="org.ametys.runtime.ui.impl.StaticContextualClientSideElement">
                 <action class="org.ametys.ribbon.control.button.ActionButton">
@@ -224,10 +225,108 @@
                 </scripts>
                 <right>LinkDirectory_Rights_Themes_Handle</right>
             </extension>
+            
+            <extension id="org.ametys.plugins.linkdirectory.LinkDirectory.moveFirst"
+                       point="org.ametys.cms.workspace.ribbon.RibbonControlsManager" 
+                       class="org.ametys.runtime.ui.impl.StaticContextualClientSideElement">
+                <action class="org.ametys.linkdirectory.MoveLinkButton">
+                    <param name="action">org.ametys.linkdirectory.Move.act</param>
+                    <param name="target-type">^link-directory$</param>
+                    <param name="enable-multiselection">false</param>
+                    <param name="role">move-first</param>
+                    
+                    <param name="label" i18n="true">PLUGINS_LINKDIRECTORY_MOVE_FIRST_LABEL</param>
+                    <param name="default-description" i18n="true">PLUGINS_LINKDIRECTORY_MOVE_FIRST_DESCRIPTION</param>
+                    
+                    <param name="icon-small" file="true">img/move_top_16.png</param>
+                    <param name="icon-medium" file="true">img/move_top_32.png</param>
+                    <param name="icon-large" file="true">img/move_top_48.png</param>
+                </action>
+                <scripts>
+                    <file>js/org/ametys/linkdirectory/MoveLinkButton.js</file>
+                    <file>js/org/ametys/linkdirectory/LinkDirectoryActions.i18n.js</file>
+                </scripts>
+                <right>LinkDirectory_Rights_Themes_Handle</right>
+            </extension>
+            
+            <extension id="org.ametys.plugins.linkdirectory.LinkDirectory.moveUp"
+                       point="org.ametys.cms.workspace.ribbon.RibbonControlsManager" 
+                       class="org.ametys.runtime.ui.impl.StaticContextualClientSideElement">
+                <action class="org.ametys.linkdirectory.MoveLinkButton">
+                    <param name="action">org.ametys.linkdirectory.Move.act</param>
+                    <param name="target-type">^link-directory$</param>
+                    <param name="enable-multiselection">false</param>
+                    <param name="role">move-up</param>
+                    
+                    <param name="label" i18n="true">PLUGINS_LINKDIRECTORY_MOVE_UP_LABEL</param>
+                    <param name="default-description" i18n="true">PLUGINS_LINKDIRECTORY_MOVE_UP_DESCRIPTION</param>
+                    
+                    <param name="icon-small" file="true">img/move_up_16.png</param>
+                    <param name="icon-medium" file="true">img/move_up_32.png</param>
+                    <param name="icon-large" file="true">img/move_up_48.png</param>
+                </action>
+                <scripts>
+                    <file>js/org/ametys/linkdirectory/MoveLinkButton.js</file>
+                    <file>js/org/ametys/linkdirectory/LinkDirectoryActions.i18n.js</file>
+                </scripts>
+                <right>LinkDirectory_Rights_Themes_Handle</right>
+            </extension>
+            
+            <extension id="org.ametys.plugins.linkdirectory.LinkDirectory.moveDown"
+                       point="org.ametys.cms.workspace.ribbon.RibbonControlsManager" 
+                       class="org.ametys.runtime.ui.impl.StaticContextualClientSideElement">
+                <action class="org.ametys.linkdirectory.MoveLinkButton">
+                    <param name="action">org.ametys.linkdirectory.Move.act</param>
+                    <param name="target-type">^link-directory$</param>
+                    <param name="enable-multiselection">false</param>
+                    <param name="role">move-down</param>
+                    
+                    <param name="label" i18n="true">PLUGINS_LINKDIRECTORY_MOVE_DOWN_LABEL</param>
+                    <param name="default-description" i18n="true">PLUGINS_LINKDIRECTORY_MOVE_DOWN_DESCRIPTION</param>
+                    
+                    <param name="icon-small" file="true">img/move_down_16.png</param>
+                    <param name="icon-medium" file="true">img/move_down_32.png</param>
+                    <param name="icon-large" file="true">img/move_down_48.png</param>
+                </action>
+                <scripts>
+                    <file>js/org/ametys/linkdirectory/MoveLinkButton.js</file>
+                    <file>js/org/ametys/linkdirectory/LinkDirectoryActions.i18n.js</file>
+                </scripts>
+                <right>LinkDirectory_Rights_Themes_Handle</right>
+            </extension>
+            
+            <extension id="org.ametys.plugins.linkdirectory.LinkDirectory.moveLast"
+                       point="org.ametys.cms.workspace.ribbon.RibbonControlsManager" 
+                       class="org.ametys.runtime.ui.impl.StaticContextualClientSideElement">
+                <action class="org.ametys.linkdirectory.MoveLinkButton">
+                    <param name="action">org.ametys.linkdirectory.Move.act</param>
+                    <param name="target-type">^link-directory$</param>
+                    <param name="enable-multiselection">false</param>
+                    <param name="role">move-last</param>
+                    
+                    <param name="label" i18n="true">PLUGINS_LINKDIRECTORY_MOVE_BOTTOM_LABEL</param>
+                    <param name="default-description" i18n="true">PLUGINS_LINKDIRECTORY_MOVE_BOTTOM_DESCRIPTION</param>
+                    
+                    <param name="icon-small" file="true">img/move_bottom_16.png</param>
+                    <param name="icon-medium" file="true">img/move_bottom_32.png</param>
+                    <param name="icon-large" file="true">img/move_bottom_48.png</param>
+                </action>
+                <scripts>
+                    <file>js/org/ametys/linkdirectory/MoveLinkButton.js</file>
+                    <file>js/org/ametys/linkdirectory/LinkDirectoryActions.i18n.js</file>
+                </scripts>
+                <right>LinkDirectory_Rights_Themes_Handle</right>
+            </extension>
         </extensions>
     </feature>
     
     <feature name="org.ametys.plugins.linkdirectory.service">
+        <components>
+            <component role="org.ametys.plugins.linkdirectory.DirectoryHelper"
+                       class="org.ametys.plugins.linkdirectory.DirectoryHelper">
+                <!-- Helper for link directory service methods -->
+            </component>
+        </components>
         <extensions>
             <extension point="org.ametys.runtime.plugins.core.right.RightsExtensionPoint"
                        id="org.ametys.plugins.linkdirectory.service.directory.rights">
@@ -239,12 +338,12 @@
             </extension>
             
             <extension point="org.ametys.web.service.ServiceExtensionPoint"
-                       class="org.ametys.web.service.StaticService"
+                       class="org.ametys.plugins.linkdirectory.LinkDirectoryService" 
                        id="org.ametys.plugins.linkdirectory.DirectoryService">
-                
+
+                <!-- LinkService -->
                 <url>service/directory.html</url>
                 
-                <cacheable>true</cacheable>
                 <right>LinkDirectory_Right_Service_Directory</right>
                 
                 <label i18n="true">PLUGINS_LINKDIRECTORY_SERVICE_DIRECTORY_LABEL</label>
@@ -264,6 +363,10 @@
                         <description i18n="true">PLUGINS_LINKDIRECTORY_SERVICE_THEMES_DESC</description>
                         <widget>link-themes</widget>
                     </parameter>
+                    <parameter name="configurable" type="boolean">
+                        <label i18n="true">PLUGINS_LINKDIRECTORY_SERVICE_CONFIGURABLE</label>
+                        <description i18n="true">PLUGINS_LINKDIRECTORY_SERVICE_CONFIGURABLE_DESC</description>
+                    </parameter>
                     <parameter name="showUrl" type="boolean">
                         <label i18n="true">PLUGINS_LINKDIRECTORY_SERVICE_DISPLAY_LINKS</label>
                         <description i18n="true">PLUGINS_LINKDIRECTORY_SERVICE_DISPLAY_LINKS_DESC</description>
@@ -285,6 +388,7 @@
                                 <path>pages/services/directory</path>
                                 <values>
                                     <value>pages/services/directory/directory_1.0.xsl</value>
+                                    <value>pages/services/directory/thumbnails_1.1.xsl</value>
                                 </values>
                             </custom-enumerator>
                         </enumeration>
@@ -305,4 +409,13 @@
         </extensions>
     </feature>
     
+    <feature name="org.ametys.plugins.linkdirectory.inputdata">
+        <extensions>
+            <extension point="org.ametys.web.inputdata.InputDataExtensionPoint"
+                       id="org.ametys.plugins.linkdirectory.LinkDirectoryInputData"
+                       class="org.ametys.plugins.linkdirectory.LinkDirectoryInputData">
+            </extension>
+        </extensions>
+    </feature>
+    
 </plugin>
Index: main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryActions.i18n.js
===================================================================
--- main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryActions.i18n.js	(revision 28682)
+++ main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryActions.i18n.js	(working copy)
@@ -1,5 +1,5 @@
 /*
- *  Copyright 2012 Anyware Services
+ *  Copyright 2015 Anyware Services
  *
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
 
 Ext.namespace('org.ametys.linkdirectory.Edit');
 Ext.namespace('org.ametys.linkdirectory.Delete');
+Ext.namespace('org.ametys.linkdirectory.Move');
 
 /**
  * @private
@@ -107,6 +108,8 @@
         var params = form.getValues();
         params.siteName = context.parameters.siteName;
         params.language = org.ametys.linkdirectory.Edit.linkLang;
+        // do 'grant-any-user' mannually since it is a checkbox, not correctly return by form.getValues()
+        params['grant-any-user'] = form.findField('grant-any-user').getValue();
         
         org.ametys.linkdirectory.Edit.dialog.showWaitMsg();
         
@@ -294,3 +297,41 @@
     var event = org.ametys.messagebus.bus.MessageBuilder.getInstance().createMessage(org.ametys.ribbon.RibbonManager.EVENTTYPE_DELETED, null, args[0]);
     org.ametys.messagebus.MessageBus.getInstance().fireMessages([event]);
 }
+
+/**
+ * Move link 
+ */
+org.ametys.linkdirectory.Move.act = function(id, config, pluginName)
+{
+    var targets = org.ametys.ribbon.RibbonManager.getInstance().getCurrentSelectionTargets();
+    var allTargets = org.ametys.messagebus.message.MessageTargetHelper.findAll(targets, function(target) { return target.getType() == 'link-directory' });
+    
+    if (allTargets.length != 1)
+    {
+        return;
+    }
+    
+    var id = allTargets[0].getParameters().id;
+    
+    var params = {
+        'id': id,
+        'siteName': context.parameters.siteName,
+        'role': config.role
+    };
+    
+    var serverMessage = new org.ametys.servercomm.ServerMessage(pluginName, '/link/move', params, org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, org.ametys.linkdirectory.Move.callback, this, [id]);
+    org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
+}
+
+org.ametys.linkdirectory.Move.callback = function(response, args)
+{
+    if (org.ametys.servercomm.ServerComm.handleBadResponse("<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_HANDLE_MOVE_LINK_ERROR'/>", response, 'org.ametys.linkdirectory.Move'))
+    {
+        return;
+    }
+    
+    var id = args[0];
+    var target = org.ametys.messagebus.bus.MessageBuilder.getInstance().createTarget('link-directory', {'id': id});
+    var event = org.ametys.messagebus.bus.MessageBuilder.getInstance().createMessage(org.ametys.ribbon.RibbonManager.EVENTTYPE_MOVED, null, [target]);
+    org.ametys.messagebus.MessageBus.getInstance().fireMessages([event]);
+}
Index: main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryDialog.i18n.js
===================================================================
--- main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryDialog.i18n.js	(revision 28682)
+++ main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryDialog.i18n.js	(working copy)
@@ -31,7 +31,7 @@
         icon: getPluginResourcesUrl(pluginName) + '/img/link-directory-16.png',
         
         width: 500,
-        height: 460,
+        height: 590, 
         autoScroll: false,
         
         items: [this._formPanel],
@@ -58,6 +58,8 @@
 
 org.ametys.linkdirectory.LinkDirectoryDialog.prototype._createFormPanel = function()
 {
+	var me = this;
+	
     var formPanel = new Ext.form.FormPanel( {
         labelWidth: 125,
         defaultType: 'textfield',
@@ -92,7 +94,8 @@
                 name: 'picture',
                 fieldLabel: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_PICTURE_LABEL'/>",
                 desc: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_PICTURE_DESC'/>",
-                width: 280
+                width: 280,
+                labelStyle: "height: 100px;"
             }),
             new org.ametys.form.TextField({
                 name: 'picture-alternative',
@@ -104,7 +107,7 @@
                 name: 'themes',
                 fieldLabel: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_THEMES_LABEL'/>",
                 desc: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_THEMES_DESC'/>",
-                width: 280,
+                width: 283,
                 modifiable: true
             }),
             new org.ametys.form.TextAreaField({
@@ -114,6 +117,33 @@
                 width: 280,
                 height: 90
             }),
+            new org.ametys.form.BooleanField({
+                name: 'grant-any-user',
+                fieldLabel: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANT_ANY_USER_LABEL'/>",
+                desc: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANT_ANY_USER_DESC'/>",
+                width: 280,
+                listeners: {'check': function(field, checked) {
+                						me._formPanel.getForm().findField('fousers').setDisabled(checked);
+                						me._formPanel.getForm().findField('fogroups').setDisabled(checked);
+                					}
+                			}
+            }),
+            new org.ametys.web.widgets.FOUserSelectorWidget({
+                name: 'fousers',
+                fieldLabel: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_USERS_LABEL'/>",
+                desc: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_USERS_DESC'/>",
+                width: 280,
+                multiple: true,
+                modifiable: true
+            }),
+            new org.ametys.web.widgets.FOGroupSelectorWidget({
+                name: 'fogroups',
+                fieldLabel: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_GROUPS_LABEL'/>",
+                desc: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_LINK_DIALOG_GRANTED_GROUPS_DESC'/>",
+                width: 280,
+                multiple: true,
+                modifiable: true,
+            })
         ],
         
         bodyStyle: 'padding:10px',
@@ -148,10 +178,15 @@
         }
     }, this);
     
+    // Reset the dialog's fields
     form.findField('themes').setThemeValue('');
     form.findField('id').setValue(id);
     
-    if (id != null &amp;&amp; id != '')
+    form.findField('fousers').setUserValue('');
+    form.findField('fogroups').setGroupValue('');
+    form.findField('grant-any-user').setValue(false);
+    
+    if (id)
     {
         var serverMessage = new org.ametys.servercomm.ServerMessage(this._pluginName, 'link.xml', {'id': id}, org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, this._initCb, this, {form: form});
         var response = org.ametys.servercomm.ServerComm.getInstance().send(serverMessage);
@@ -219,7 +254,32 @@
         ids.push(themeNodes[i].getAttribute('id'));
         labels.push(themeNodes[i].getAttribute('label'));
     }
+    
     form.findField('themes').setThemeValue(ids, labels);
     
     form.findField('content').setValue(content);
+    
+    // Update access limitations
+    var grantAnyUser = linkNode.getAttribute('grantAnyUser') || false;
+    form.findField('grant-any-user').setValue(grantAnyUser);
+    
+    var grantedUserNodes = linkNode.selectNodes('granted-users/granted-user');
+    var logins = [];
+    var fullNames = [];
+    for (var i = 0; i &lt; grantedUserNodes.length; i++)
+	{
+    	logins.push(grantedUserNodes[i].getAttribute('login'));
+    	fullNames.push(grantedUserNodes[i].getAttribute('fullName'));
+	}
+    form.findField('fousers').setUserValue(logins, fullNames);
+    
+    var grantedGroupNodes = linkNode.selectNodes('granted-groups/granted-group');
+    var ids = [];
+    var labels = [];
+    for (var i = 0; i &lt; grantedGroupNodes.length; i++)
+	{
+    	ids.push(grantedUserNodes[i].getAttribute('id'));
+    	labels.push(grantedUserNodes[i].getAttribute('label'));
+	}
+    form.findField('fogroups').setGroupValue(ids, labels);
 }
Index: main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryMessageTargetFactory.i18n.js
===================================================================
--- main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryMessageTargetFactory.i18n.js	(revision 28682)
+++ main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryMessageTargetFactory.i18n.js	(working copy)
@@ -53,6 +53,7 @@
    parameters['url'] = link.getAttribute("url"); 
    parameters['title'] = link.getAttribute("title"); 
    parameters['alternative'] = link.getAttribute("alternative"); 
+   parameters['limitedAccess'] = link.getAttribute("limitedAccess"); 
    parameters['themes'] = link.selectSingleNode("themes")[org.ametys.servercomm.ServerComm.xmlTextContent]; 
 	   
    return target;
Index: main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryTool.i18n.js
===================================================================
--- main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryTool.i18n.js	(revision 28682)
+++ main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkDirectoryTool.i18n.js	(working copy)
@@ -1,5 +1,5 @@
 /*
- *  Copyright 2012 Anyware Services
+ *  Copyright 2015 Anyware Services
  *
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -23,6 +23,10 @@
 
 Ext.extend(org.ametys.linkdirectory.LinkDirectoryTool, org.ametys.uitool.tool.Tool, {});
 
+/**
+ * List of links predecessors and successors, indexed by link ID.
+ * @private
+ */
 org.ametys.linkdirectory.LinkDirectoryTool.prototype.createPanel = function()
 {
     // The mapping for XML doc
@@ -31,7 +35,8 @@
         {name: 'url', mapping: '@url'},
         {name: 'title', mapping: '@title'},
         {name: 'alternative', mapping: '@alternative'},
-        {name: 'themes', mapping: 'themes'}
+        {name: 'themes', mapping: 'themes'},
+        {name: 'limitedAccess', mapping: '@limitedAccess'}
     ]);
     
     // HTTP proxy
@@ -51,8 +56,6 @@
             },
             mapping
         ),
-        
-        sortInfo: {field: 'title', direction: 'ASC'}
     });
     
     this._xmlStore.on('beforeload', this._onBeforeLoad, this);
@@ -82,14 +85,18 @@
         store: this._xmlStore,
         
         view: new Ext.grid.GridView({
-            forceFit: true
+        	forceFit: true,
+	        enableGroupingMenu : false,
+	        showGroupName: false,
+	    	hideGroupedColumn: true
         }),
         
         columns: [
-            {header: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_UITOOL_URL'/>", width: 160, sortable: true, dataIndex: 'url'},
-            {header: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_UITOOL_TITLE'/>", width: 100, sortable: true, dataIndex: 'title'},
-            {header: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_UITOOL_ALTERNATIVE'/>", width: 100, sortable: true, dataIndex: 'alternative'},
-            {header: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_UITOOL_THEMES'/>", width: 100, sortable: true, dataIndex: 'themes'}
+            {header: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_UITOOL_URL'/>", width: 160, dataIndex: 'url'},
+            {header: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_UITOOL_TITLE'/>", width: 100, dataIndex: 'title'},
+            {header: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_UITOOL_ALTERNATIVE'/>", width: 100, dataIndex: 'alternative'},
+            {header: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_UITOOL_THEMES'/>", width: 100, dataIndex: 'themes'},
+            {header: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_UITOOL_LIMITED_ACCESS'/>", width: 25, dataIndex: 'limitedAccess'}
         ]
     });
     this._grid.getSelectionModel().on('selectionchange', this._onSelectLink, this);
@@ -137,13 +144,20 @@
 org.ametys.linkdirectory.LinkDirectoryTool.prototype._sendSelectionMessage = function(records)
 {
     var targets = [];
-    for (var i=0; i &lt; records.length; i++)
+    for (var i = 0; i &lt; records.length; i++)
     {
-        targets.push(org.ametys.messagebus.bus.MessageBuilder.getInstance().createTarget('link-directory', {'id': records[i].data.id}));
+    	var parameters = {},
+    	link = records[i];
+    	
+    	parameters.id = link.data.id;
+    	parameters.index = link.store.indexOf(link);
+    	parameters.size = link.store.totalLength;
+    	
+        targets.push(org.ametys.messagebus.bus.MessageBuilder.getInstance().createTarget('link-directory', parameters));
     }
-    
+	
     var event = org.ametys.messagebus.bus.MessageBuilder.getInstance().createMessage(org.ametys.ribbon.RibbonManager.EVENTTYPE_SELECTIONCHANGED, null, targets);
-    org.ametys.messagebus.MessageBus.getInstance().fireMessages([event]);
+	org.ametys.messagebus.MessageBus.getInstance().fireMessages([event]);
 }
 
 org.ametys.linkdirectory.LinkDirectoryTool.prototype.onMessage = function(messages)
@@ -180,6 +194,10 @@
                 this._removeLink(id);
             } 
         }
+		else if (messages[i].getType().getName() == org.ametys.ribbon.RibbonManager.EVENTTYPE_MOVED)
+		{
+		    this.refresh();
+		}
     }
 }
 
@@ -202,8 +220,11 @@
         record.set('title', params.title);
         record.set('alternative', params.alternative);
         record.set('themes', params.themes);
+        record.set('limitedAccess', params.limitedAccess);
         record.commit();
     }
+    
+    this._updateSelections();
 }
 
 /**
@@ -219,11 +240,14 @@
         'url': params.url,
         'title': params.title,
 		'alternative': params.alternative,
-		'themes': params.themes
+		'themes': params.themes,
+		'limitedAccess': params.limitedAccess
     }, params.id);
     
     this._grid.getStore().addSorted(record);
     this._grid.getSelectionModel().selectRecords([record]);
+
+    this._updateSelections();
 }
 
 
@@ -383,6 +407,7 @@
 org.ametys.linkdirectory.LinkDirectoryTool.prototype._onLoad = function(store, records, options)
 {
     this.upToDate();
+    this._updateSelections();
 }
 
 /**
@@ -392,12 +417,7 @@
 org.ametys.linkdirectory.LinkDirectoryTool.prototype._onFocus = function(messages)
 {
     this._hasFocus = true;
-    
-    var records = this._grid.getSelectionModel().getSelections();
-    if (records.length &gt; 0)
-    {
-        this._sendSelectionMessage(records);
-    }
+    this._updateSelections();
 }
 
 /**
@@ -422,3 +442,16 @@
 		eventBus.fireMessages([event]);
 	}
 }
+
+/**
+ * @private
+ * Update the selections
+ */
+org.ametys.linkdirectory.LinkDirectoryTool.prototype._updateSelections = function ()
+{
+	var records = this._grid.getSelectionModel().getSelections();
+	if (records.length &gt; 0)
+	{
+	    this._sendSelectionMessage(records);
+	}
+}
Index: main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkThemesDialog.i18n.js
===================================================================
--- main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkThemesDialog.i18n.js	(revision 29041)
+++ main/plugin-link-directory/resources/js/org/ametys/linkdirectory/LinkThemesDialog.i18n.js	(working copy)
@@ -1,5 +1,5 @@
 /*
- *  Copyright 2012 Anyware Services
+ *  Copyright 2015 Anyware Services
  *
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -71,7 +71,8 @@
 {
     var mapping = Ext.data.Record.create([
         {name: 'id', mapping: '@id'},
-        {name: 'label', mapping: '@label'}
+        {name: 'label', mapping: '@label'},
+        {name: 'name', mapping: '@name'}
     ]);
     
     var proxy = new Ext.data.HttpProxy({
@@ -105,7 +106,12 @@
     {
         columns.push(checkModel);
     }
-    columns.push({header: "<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_THEME_COLUMN_NAME"/>", width: 300, menuDisabled: true, sortable: true, dataIndex: 'label'});
+    
+    // Labels
+    columns.push({header: "<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_THEME_COLUMN_NAME"/>", width: 160, menuDisabled: true, sortable: true, dataIndex: 'label'});
+    
+    // Names
+    columns.push({header: "<i18n:text i18n:key="PLUGINS_LINKDIRECTORY_THEME_ID_COLUMN_NAME"/>", width: 159, menuDisabled: true, sortable: true, dataIndex: 'name'});
     
     var sm;
     if (this._selectable)
@@ -120,7 +126,6 @@
     var listView = new org.ametys.ListView({
         region: 'center',
         store: this._store,
-        hideHeaders: true,
         cm: new Ext.grid.ColumnModel({
             defaults: {
                 width: 20,
@@ -222,7 +227,8 @@
     {
         themes.push({
             id: selectedThemes[i].data.id,
-            label: selectedThemes[i].data.label
+            label: selectedThemes[i].data.label,
+            name: selectedThemes[i].data.name
         });
     }
     
@@ -245,10 +251,20 @@
 
 org.ametys.linkdirectory.LinkThemesDialog.prototype._selectOnLoad = function(records, options, success)
 {
-    if (success)
-    {
-        this.setSelected(options.values);
-    }
+	
+	if (success)
+	{
+		// Select the last added entry
+	    if (options.ignoreSelectable)
+		{
+			this._list.getSelectionModel().selectRow(this._store.indexOfId(options.values[0]));
+			return;
+		}
+	    else
+	    {
+	        this.setSelected(options.values);
+	    }
+	}
 }
 
 org.ametys.linkdirectory.LinkThemesDialog.prototype._onSelect = function(sm)
@@ -273,7 +289,18 @@
         fn: function(btn, text, opts) {
             if (btn == 'ok')
             {
-                this._doAddTheme(text);
+	            if (text === null || text === "")
+            	{
+		            Ext.Msg.show({
+		                title: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_EMPTY_THEME_TITLE'/>",
+		                msg: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_EMPTY_THEME_MSG'/>",
+		                buttons: Ext.Msg.OK,
+		                icon: Ext.MessageBox.ERROR
+		            });
+		            return;
+            	}
+	            
+	            this._doAddTheme(text);
             }
         },
         scope: this
@@ -299,7 +326,8 @@
         return;
     }
     
-    var error = response.selectSingleNode('ActionResult/error')[org.ametys.servercomm.ServerComm.xmlTextContent];
+    var error = response.selectSingleNode('ActionResult/error')[org.ametys.servercomm.ServerComm.xmlTextContent],
+        id = response.selectSingleNode('ActionResult/id')[org.ametys.servercomm.ServerComm.xmlTextContent];
     
     if (error == 'theme-already-exists')
     {
@@ -312,7 +340,14 @@
         return;
     }
     
-    this._store.load();
+    var opts = {};
+    
+    opts.callback = this._selectOnLoad;
+    opts.scope = this;
+    opts.values =  [id];
+    opts.ignoreSelectable = true;
+    
+    this._store.load(opts);
 }
 
 org.ametys.linkdirectory.LinkThemesDialog.prototype._editTheme = function()
@@ -346,6 +381,17 @@
 
 org.ametys.linkdirectory.LinkThemesDialog.prototype._doEditTheme = function(id, text)
 {
+	if (text === null || text === "")
+	{
+        Ext.Msg.show({
+            title: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_EMPTY_THEME_TITLE'/>",
+            msg: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_EMPTY_THEME_MSG'/>",
+            buttons: Ext.Msg.OK,
+            icon: Ext.MessageBox.ERROR
+        });
+        return;
+	}
+	
     var params = {
         id: id,
         label: text,
@@ -369,7 +415,7 @@
     if (error == 'theme-already-exists')
     {
         Ext.Msg.show({
-            title: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_THEME_EDIT_TITLE'/>",
+            title: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_EMPTY_THEME_TITLE'/>",
             msg: "<i18n:text i18n:key='PLUGINS_LINKDIRECTORY_THEME_ERROR_ALREADY_EXISTS'/>",
             buttons: Ext.Msg.OK,
             icon: Ext.MessageBox.ERROR
@@ -378,6 +424,8 @@
     }
     
     this._store.load();
+    
+    this._fireMessages(response);
 }
 
 org.ametys.linkdirectory.LinkThemesDialog.prototype._removeThemes = function()
@@ -432,7 +480,15 @@
     
     this._store.load();
     
-    // Fire messages
+    this._fireMessages(response);
+}
+
+/**
+ * Fire the metadata changed messages from the server's response
+ */
+org.ametys.linkdirectory.LinkThemesDialog.prototype._fireMessages = function(response)
+{
+	// Fire messages
     var modifiedLinks = response.selectSingleNode('ActionResult/modified-links')[org.ametys.servercomm.ServerComm.xmlTextContent];
     if (modifiedLinks != '')
     {
Index: main/plugin-link-directory/resources/js/org/ametys/linkdirectory/MoveLinkButton.js
===================================================================
--- main/plugin-link-directory/resources/js/org/ametys/linkdirectory/MoveLinkButton.js	(revision 0)
+++ main/plugin-link-directory/resources/js/org/ametys/linkdirectory/MoveLinkButton.js	(revision 0)
@@ -0,0 +1,62 @@
+/*
+ *  Copyright 2015 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+Ext.namespace('org.ametys.linkdirectory');
+
+/**
+ * Enable or disable link reordering buttons depending on the link position.
+ */
+org.ametys.linkdirectory.MoveLinkButton = function(id, configuration, pluginName)
+{
+	org.ametys.linkdirectory.MoveLinkButton.superclass.constructor.call(this, id, configuration, pluginName);
+}
+
+Ext.extend(org.ametys.linkdirectory.MoveLinkButton, org.ametys.ribbon.control.button.ActionSelectionButton, {});
+
+org.ametys.linkdirectory.MoveLinkButton.prototype.onMessage = function(messages)
+{
+    for (var i = messages.length-1; i >= 0; i--)
+    {
+        if (messages[i].getType().getName() == org.ametys.ribbon.RibbonManager.EVENTTYPE_SELECTIONCHANGED)
+        {
+            var allTargets = messages[i].getTargets();
+            var targets = org.ametys.messagebus.message.MessageTargetHelper.findAll(allTargets, this._testTarget.createDelegate(this));
+            
+            if (targets.length == 1 && this._testLink(targets[0]))
+            {
+                this.getElements().each(function(element) { element.enable(); });
+            }
+            else
+            {
+                this.getElements().each(function(element) { element.disable(); });
+            }
+        }
+    }
+}
+
+/**
+ * Test a link on a button.
+ * @return {org.ametys.messagebus.message.MessageTarget} The target to check
+ * @private
+ */
+org.ametys.linkdirectory.MoveLinkButton.prototype._testLink = function(target)
+{
+	var role = this.getConfiguration().role,
+        index = target.getParameters().index,
+        size= target.getParameters().size;
+        
+    return (((role == 'move-up' || role == 'move-first') && index != 0) || ((role == 'move-down' || role == 'move-last') && index != size - 1)) ? true : false;
+}
Index: main/plugin-link-directory/sitemap.xmap
===================================================================
--- main/plugin-link-directory/sitemap.xmap	(revision 28682)
+++ main/plugin-link-directory/sitemap.xmap	(working copy)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
-   Copyright 2012 Anyware Services
+   Copyright 2015 Anyware Services
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
@@ -20,8 +20,6 @@
         <map:generators>
             <map:generator name="link" src="org.ametys.plugins.linkdirectory.link.LinkGenerator" logger="org.ametys.plugins.linkdirectory.link.LinkGenerator"/>
             <map:generator name="link-list" src="org.ametys.plugins.linkdirectory.link.LinkListGenerator" logger="org.ametys.plugins.linkdirectory.link.LinkListGenerator"/>
-            
-            <map:generator name="theme" src="org.ametys.plugins.linkdirectory.theme.ThemeGenerator" logger="org.ametys.plugins.linkdirectory.theme.ThemeGenerator"/>
             <map:generator name="theme-list" src="org.ametys.plugins.linkdirectory.theme.ThemeListGenerator" logger="org.ametys.plugins.linkdirectory.theme.ThemeListGenerator"/>
         </map:generators>
         
@@ -31,10 +29,13 @@
             <map:action name="create-link" src="org.ametys.plugins.linkdirectory.link.CreateLinkAction" logger="org.ametys.plugins.linkdirectory.link.CreateLinkAction"/>
             <map:action name="update-link" src="org.ametys.plugins.linkdirectory.link.UpdateLinkAction" logger="org.ametys.plugins.linkdirectory.link.UpdateLinkAction"/>
             <map:action name="delete-link" src="org.ametys.plugins.linkdirectory.link.DeleteLinkAction" logger="org.ametys.plugins.linkdirectory.link.DeleteLinkAction"/>
-
+            <map:action name="move-link"   src="org.ametys.plugins.linkdirectory.link.MoveLinkAction"   logger="org.ametys.plugins.linkdirectory.link.MoveLinkAction"/>
+            
             <map:action name="create-theme" src="org.ametys.plugins.linkdirectory.theme.CreateThemeAction" logger="org.ametys.plugins.linkdirectory.theme.CreateThemeAction"/>
             <map:action name="update-theme" src="org.ametys.plugins.linkdirectory.theme.UpdateThemeAction" logger="org.ametys.plugins.linkdirectory.theme.UpdateThemeAction"/>
             <map:action name="delete-theme" src="org.ametys.plugins.linkdirectory.theme.DeleteThemeAction" logger="org.ametys.plugins.linkdirectory.theme.DeleteThemeAction"/>
+            
+            <map:action name="set-userprefs" src="org.ametys.plugins.linkdirectory.LinkDirectorySetUserPreferencesAction" logger="org.ametys.plugins.linkdirectory.LinkDirectorySetUserPreferencesAction"/>
         </map:actions>
     </map:components>
 
@@ -64,6 +65,18 @@
                 <map:serialize type="xhtml"/>
             </map:match>
             
+            <!-- Service's user preferences -->
+            <map:match pattern="set-user-preferences">
+			    <map:act type="set-userprefs">
+			        <map:generate type="action-result" label="xml"/>
+			        <map:transform type="i18n">
+	                    <map:parameter name="locale" value="{request-attr:renderingLanguage}"/>
+	                    <map:parameter name="plugin" value="{request-attr:pluginName}"/>
+			        </map:transform>
+	                <map:serialize type="xml"/>
+			    </map:act>
+			</map:match>
+            
             <map:match pattern="**">
                 <map:act type="check-not-front"/>
             </map:match>
@@ -103,10 +116,18 @@
                 </map:act>
             </map:match>
             
-            <!-- Themes -->
-            <map:match pattern="theme.xml">
-                <map:generate type="theme"/>
-                <map:serialize type="xml"/>
+            <map:match pattern="link/move">
+                <map:act type="move-link">
+                    <map:parameter name="siteName" value="{parent-context-attr:siteName}"/>
+                    <map:parameter name="id" value="{parent-context-attr:id}"/>
+                    <map:parameter name="previousSiblingId" value="{parent-context-attr:previousSiblingId}"/>
+                    <map:parameter name="nextSiblingId" value="{parent-context-attr:nextSiblingId}"/>
+                    <map:parameter name="role" value="{parent-context-attr:role}"/>
+                    <map:generate type="action-result">
+                        <map:parameter name="msg" value="{msg}"/>
+                    </map:generate>
+                    <map:serialize type="xml"/> 
+                </map:act>
             </map:match>
             
             <map:match pattern="theme/create">
@@ -124,6 +145,7 @@
                     <map:generate type="action-result">
                         <map:parameter name="id" value="{id}"/>
                         <map:parameter name="error" value="{error}"/>
+                        <map:parameter name="modified-links" value="{modified-links}"/>
                     </map:generate>
                     <map:serialize type="xml"/>
                 </map:act>
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/DirectoryHelper.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/DirectoryHelper.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/DirectoryHelper.java	(working copy)
@@ -1,5 +1,5 @@
 /*
- *  Copyright 2012 Anyware Services
+ *  Copyright 2015 Anyware Services
  *
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -15,30 +15,121 @@
  */
 package org.ametys.plugins.linkdirectory;
 
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.avalon.framework.component.Component;
+import org.apache.avalon.framework.context.Context;
+import org.apache.avalon.framework.context.ContextException;
+import org.apache.avalon.framework.context.Contextualizable;
+import org.apache.avalon.framework.logger.LogEnabled;
+import org.apache.avalon.framework.logger.Logger;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.avalon.framework.service.Serviceable;
+import org.apache.cocoon.components.ContextHelper;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.xml.AttributesImpl;
+import org.apache.cocoon.xml.XMLUtils;
+import org.apache.commons.collections.IteratorUtils;
+import org.apache.commons.lang.ArrayUtils;
 import org.apache.commons.lang.StringUtils;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
 
+import org.ametys.plugins.explorer.resources.Resource;
+import org.ametys.plugins.linkdirectory.repository.DefaultLink;
 import org.ametys.plugins.linkdirectory.repository.DefaultLinkFactory;
+import org.ametys.plugins.linkdirectory.repository.DefaultTheme;
 import org.ametys.plugins.linkdirectory.repository.DefaultThemeFactory;
+import org.ametys.plugins.linkdirectory.theme.ThemeExpression;
+import org.ametys.plugins.repository.AmetysObject;
+import org.ametys.plugins.repository.AmetysObjectIterable;
+import org.ametys.plugins.repository.AmetysObjectResolver;
 import org.ametys.plugins.repository.AmetysRepositoryException;
 import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
+import org.ametys.plugins.repository.TraversableAmetysObject;
+import org.ametys.plugins.repository.UnknownAmetysObjectException;
+import org.ametys.plugins.repository.metadata.BinaryMetadata;
 import org.ametys.plugins.repository.query.expression.Expression;
+import org.ametys.plugins.repository.query.expression.OrExpression;
+import org.ametys.runtime.group.GroupsManager;
+import org.ametys.runtime.plugins.core.userpref.UserPreferencesException;
+import org.ametys.runtime.plugins.core.userpref.UserPreferencesManager;
+import org.ametys.runtime.user.User;
+import org.ametys.runtime.user.UsersManager;
+import org.ametys.runtime.util.I18nUtils;
+import org.ametys.runtime.util.I18nizableText;
+import org.ametys.web.WebConstants;
+import org.ametys.web.renderingcontext.RenderingContext;
+import org.ametys.web.renderingcontext.RenderingContextHandler;
+import org.ametys.web.repository.page.Page;
 import org.ametys.web.repository.site.Site;
+import org.ametys.web.repository.site.SiteManager;
+import org.ametys.web.userpref.FOUserPreferencesConstants;
 
 /**
  * Link directory helper.
  */
-public final class DirectoryHelper
+public final class DirectoryHelper implements Component, Contextualizable, LogEnabled, Serviceable
 {
-    
     private static final String __PLUGIN_NODE_NAME = "linkdirectory";
     
     private static final String __LINKS_NODE_NAME = "ametys:directoryLinks";
     
     private static final String __THEMES_NODE_NAME = "ametys:themes";
     
-    private DirectoryHelper()
+    /** The logger */
+    private static Logger _logger;
+    
+    /** The Ametys object resolver */
+    private static AmetysObjectResolver _ametysObjectResolver;
+    
+    /** The users manager */
+    private static UsersManager _usersManager;
+
+    /** The groups manager */
+    private static GroupsManager _groupsManager;
+    
+    /** The site manager */
+    private static SiteManager _siteManager;
+    
+    /** I18nUtils */
+    private static I18nUtils _i18nUtils;
+    
+    /** The rendering context handler */
+    private static RenderingContextHandler _renderingContextHandler;
+    
+    /** The user preferences manager */
+    private static UserPreferencesManager _userPreferencesManager;
+    
+    /** The Avalon context */
+    private static Context _context;
+    
+    
+    public void contextualize(Context context) throws ContextException
     {
-        // Hides the default constructor.
+        _context = context;
+    }
+    
+    public void enableLogging(Logger logger)
+    {
+        _logger = logger;
+    }
+    
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
+        _usersManager = (UsersManager) manager.lookup(UsersManager.ROLE + ".FO");
+        _groupsManager = (GroupsManager) manager.lookup(GroupsManager.ROLE + ".FO");
+        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
+        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
+        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
+        _userPreferencesManager = (UserPreferencesManager) manager.lookup(UserPreferencesManager.ROLE + ".FO");
     }
     
     /**
@@ -62,10 +153,10 @@
     }
     
     /**
-     * Get the word definitions storage node.
+     * Get the links root node.
      * @param site the site
      * @param language the language.
-     * @return the word definitions storage node.
+     * @return the links root node.
      * @throws AmetysRepositoryException if a repository error occurs.
      */
     public static ModifiableTraversableAmetysObject getLinksNode(Site site, String language) throws AmetysRepositoryException
@@ -83,15 +174,15 @@
         }
         catch (AmetysRepositoryException e)
         {
-            throw new AmetysRepositoryException("Error getting the word definitions node for site " + site.getName() + " and language " + language, e);
+            throw new AmetysRepositoryException("Error getting the link directory root node for site " + site.getName() + " and language " + language, e);
         }
     }
 
     /**
-     * Get the word definitions storage node.
+     * Get the themes storage node.
      * @param site the site
      * @param language the language.
-     * @return the word definitions storage node.
+     * @return the themes storage node.
      * @throws AmetysRepositoryException if a repository error occurs.
      */
     public static ModifiableTraversableAmetysObject getThemesNode(Site site, String language) throws AmetysRepositoryException
@@ -114,9 +205,9 @@
     }
     
     /**
-     * Get the word definitions storage node.
+     * Get the plugin node path
      * @param siteName the site name.
-     * @return the word definitions storage node.
+     * @return the plugin node path.
      */
     public static String getPluginNodePath(String siteName)
     {
@@ -124,10 +215,10 @@
     }
     
     /**
-     * Get the word definitions storage node.
+     * Get the links root node path
      * @param siteName the site name.
-     * @param language the language.
-     * @return the word definitions storage node.
+     * @param language the language
+     * @return the links root node path.
      */
     public static String getLinksNodePath(String siteName, String language)
     {
@@ -135,10 +226,10 @@
     }
     
     /**
-     * Get the word definitions storage node.
+     * Get the themes node path
      * @param siteName the site name.
-     * @param language the language.
-     * @return the word definitions storage node.
+     * @param language the language
+     * @return the themes node path.
      */
     public static String getThemesNodePath(String siteName, String language)
     {
@@ -146,19 +237,42 @@
     }
     
     /**
-     * Get the word definitions storage node.
+     * Get all the links
+     * @param siteName the site name.
+     * @param language the language.
+     * @return all the links' nodes
+     */
+    public static String getAllLinksQuery(String siteName, String language)
+    {
+        return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")";
+    }
+    
+    /**
+     * Get the link query corresponding to the expression passed as a parameter
      * @param siteName the site name.
      * @param language the language.
      * @param expression 
-     * @return the word definitions storage node.
+     * @return the link corresponding to the expression passed as a parameter
+     */
+    public static String getLinksQuery(String siteName, String language, Expression expression)
+    {
+        return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[" + expression.build() + "]";
+    }
+    
+    /**
+     * Get the theme query corresponding to the expression passed as a parameter
+     * @param siteName the site name.
+     * @param language the language.
+     * @param expression 
+     * @return the theme corresponding to the expression passed as a parameter
      */
-    public static String getLinkQuery(String siteName, String language, Expression expression)
+    public static String getThemeQuery(String siteName, String language, Expression expression)
     {
         return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[" + expression.build() + "]";
     }
     
     /**
-     * Get the word definitions storage node.
+     * Get the query verifying the existence of an url
      * @param siteName the site name.
      * @param language the language.
      * @param url the url to test. 
@@ -166,36 +280,531 @@
      */
     public static String getUrlExistsQuery(String siteName, String language, String url)
     {
-        String lowerUrl = StringUtils.replace(url, "'", "''").toLowerCase();
-        return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[fn:lower-case(@ametys-internal:url) = '" + lowerUrl + "']";
+        String lowerCaseUrl = StringUtils.replace(url, "'", "''").toLowerCase();
+        return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[fn:lower-case(@ametys-internal:url) = '" + lowerCaseUrl + "']";
     }
     
     /**
-     * Get the word definitions storage node.
+     * Get the query verifying the existence of a theme
      * @param siteName the site name.
      * @param language the language.
-     * @param label the label to test. 
+     * @param label the label of the theme to test. 
      * @return the word definitions storage node.
      */
     public static String getThemeExistsQuery(String siteName, String language, String label)
     {
-        String lowerLabel = StringUtils.replace(label, "'", "''").toLowerCase();
-        return getThemesNodePath(siteName, language) + "/element(*, " + DefaultThemeFactory.THEME_NODE_TYPE + ")[fn:lower-case(@ametys-internal:label) = '" + lowerLabel + "']";
+        String lowerCaseLabel = StringUtils.replace(label, "'", "''").toLowerCase();
+        return getThemesNodePath(siteName, language) + "/element(*, " + DefaultThemeFactory.THEME_NODE_TYPE + ")[fn:lower-case(@ametys-internal:label) = '" + lowerCaseLabel + "']";
+    }
+    
+    /**
+     * Normalizes an input string in order to capitalize it, remove accents, and replace whitespaces with underscores
+     * @param s the string to normalize
+     * @return the normalized string
+     */
+    public static String normalizeString(String s)
+    {
+        // Upper case
+        String upperCaseLabel = s.replaceAll(" ", "_").toUpperCase();
+        
+        // Strip accents
+        String normalizedLabel = Normalizer.normalize(upperCaseLabel, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "");
+        
+        return normalizedLabel;
+    }
+    
+    /**
+     * Get the list of links corresponding to the given theme ids
+     * @param themesIds the ids of the configured themes
+     * @param login the login of the current user
+     * @param siteName the site's name
+     * @param language the site's language
+     * @return the list of default links corresponding to the given themes
+     */
+    public static List<DefaultLink> getLinks(List<String> themesIds, String login, String siteName, String language)
+    {
+        if (themesIds.isEmpty())
+        {
+            Site site = _siteManager.getSite(siteName);
+            TraversableAmetysObject linksNode = DirectoryHelper.getLinksNode(site, language);
+            AmetysObjectIterable<AmetysObject> links = linksNode.getChildren();
+            return IteratorUtils.toList(links);
+        }
+        else
+        {
+            int configuredThemeIdsSize = themesIds.size();
+            
+            Expression[] expressions = new Expression[configuredThemeIdsSize];
+            for (int i = 0; i < themesIds.size(); i++)
+            {
+                expressions[i] = new ThemeExpression(themesIds.get(i));
+            }
+            
+            String xPathQuery = getLinksQuery(siteName, language, new OrExpression(expressions));
+            AmetysObjectIterable<AmetysObject> configuredThemeLinks = _ametysObjectResolver.query(xPathQuery);
+            
+            return IteratorUtils.toList(configuredThemeLinks);
+        }
+    }
+    
+    /**
+     * Checks if the links displayed in a link directory service has access restrictions
+     * @param siteName the name of the site
+     * @param language the language
+     * @param themesIds the list of selected theme ids
+     * @return true if the links of the service have access restrictions, false otherwise
+     */
+    public static boolean hasRestrictions(String siteName, String language, List<String> themesIds)
+    {
+        AmetysObjectIterable<AmetysObject> links = null;
+        
+        // No themes => we check all the links' access restrictions
+        if (themesIds.isEmpty())   
+        {
+            String allLinksQuery = getAllLinksQuery(siteName, language);
+            links = _ametysObjectResolver.query(allLinksQuery);
+            
+            if (isAccessRestricted(links))
+            {
+                return true;
+            }
+        }
+        // The service has themes specified => we solely check the corresponding links' access restrictions
+        else
+        {
+            for (String themeId : themesIds)
+            {
+                String xPathQuery = getLinksQuery(siteName, language, new ThemeExpression(themeId));
+                links = _ametysObjectResolver.query(xPathQuery);
+                
+                if (isAccessRestricted(links))
+                {
+                    return true;
+                }
+            }
+        }
+        
+        // All the tested links have no restricted access
+        return false;
+    }
+    
+    /**
+     * Check if the links' access is restricted or not
+     * @param links the links to be tested
+     * @return true if the link has a restricted access, false otherwise
+     */
+    public static boolean isAccessRestricted(AmetysObjectIterable<AmetysObject> links)
+    {
+        while (links.hasNext())
+        {
+            DefaultLink link = (DefaultLink) links.next();
+            
+            // If any of the links has a limited access, the service declares itself non-cacheable
+            if (link.isAllowedAnyUser() || ArrayUtils.isNotEmpty(link.getGrantedGroups()) || ArrayUtils.isNotEmpty(link.getGrantedUsers()))
+            {
+                return false;
+            }
+        }
+        
+        return true;
     }
     
     private static ModifiableTraversableAmetysObject getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException
     {
-        ModifiableTraversableAmetysObject definitionsNode;
+        ModifiableTraversableAmetysObject node;
         if (parentNode.hasChild(nodeName))
         {
-            definitionsNode = parentNode.getChild(nodeName);
+            node = parentNode.getChild(nodeName);
         }
         else
         {
-            definitionsNode = parentNode.createChild(nodeName, nodeType);
+            node = parentNode.createChild(nodeName, nodeType);
             parentNode.saveChanges();
         }
-        return definitionsNode;
+        return node;
+    }
+    
+    /**
+     * Sax the directory links
+     * @param contentHandler the content handler
+     * @param links the list of links to sax 
+     * @param storageContext the storage context, null if there is no connected user
+     * @param contextVars the context variables
+     * @param login the login of the user
+     * @param saxThemesLabelOnly true to sax themes label only
+     * @param saxLinksOnly true if the only the links need to be saxed, false otherwise
+     * @throws SAXException If an error occurs while generating the SAX events
+     * @throws UserPreferencesException if an exception occurs while getting the user preferences
+     */
+    public static void saxLinks(ContentHandler contentHandler, List<DefaultLink> links, Map<String, String> contextVars, String storageContext, String login, boolean saxThemesLabelOnly, boolean saxLinksOnly) throws SAXException, UserPreferencesException
+    {
+        String prefValues = null;
+        String[] prefLinksIdsArray = null; 
+        if (login != null)
+        {
+            prefValues =  _userPreferencesManager.getUserPreferenceAsString(login, storageContext, contextVars, "checked-links");
+            prefLinksIdsArray = StringUtils.split(prefValues, ",");
+        }
+            
+        for (DefaultLink link : links)
+        {
+            if (_isCurrentUserGrantedAccess(link))
+            {
+                boolean selected = false;
+                if (ArrayUtils.isNotEmpty(prefLinksIdsArray))
+                {
+                    if (ArrayUtils.contains(prefLinksIdsArray, link.getId()))
+                    {
+                        selected = true;
+                    }
+                }
+                
+                saxLink(contentHandler, link, saxThemesLabelOnly, selected, saxLinksOnly);
+            }
+        }
+    }
+    
+    /**
+     * Generate a directory link.
+     * @param contentHandler the content handler
+     * @param link the link to generate.
+     * @param saxThemesLabelOnly true to sax themes label only
+     * @param selected true if a front end user has checked this link as a user preference, false otherwise
+     * @param saxLinksOnly true if we want to sax a link for an input data
+     * @throws SAXException If an error occurs while generating the SAX events
+     */
+    public static void saxLink (ContentHandler contentHandler, DefaultLink link, boolean saxThemesLabelOnly, boolean selected, boolean saxLinksOnly) throws SAXException
+    {
+        AttributesImpl attrs = new AttributesImpl();
+        attrs.addCDATAAttribute("id", link.getId());
+        
+        attrs.addCDATAAttribute("url", StringUtils.defaultString(link.getUrl()));
+        attrs.addCDATAAttribute("title", StringUtils.defaultString(link.getTitle()));
+        attrs.addCDATAAttribute("content", StringUtils.defaultString(link.getContent()));
+        
+        attrs.addCDATAAttribute("alternative", StringUtils.defaultString(link.getAlternative()));
+        attrs.addCDATAAttribute("pictureAlternative", StringUtils.defaultString(link.getPictureAlternative()));
+        
+        attrs.addCDATAAttribute("user-selected", selected ? "true" : "false");
+        
+        String pictureType = link.getPictureType();
+        attrs.addCDATAAttribute("pictureType", pictureType);
+        if (pictureType.equals("resource"))
+        {
+            String resourceId = link.getResourcePictureId();
+            Resource resource = _ametysObjectResolver.resolveById(resourceId);
+            attrs.addCDATAAttribute("pictureId", resourceId);
+            attrs.addCDATAAttribute("pictureName", resource.getName());
+            attrs.addCDATAAttribute("pictureSize", Long.toString(resource.getLength()));
+            attrs.addCDATAAttribute("imageType", "explorer");
+        }
+        else if (pictureType.equals("external"))
+        {
+            BinaryMetadata picMeta = link.getExternalPicture();
+            attrs.addCDATAAttribute("picturePath", DefaultLink.METADATA_PICTURE);
+            attrs.addCDATAAttribute("pictureName", picMeta.getFilename());
+            attrs.addCDATAAttribute("pictureSize", Long.toString(picMeta.getLength()));
+            attrs.addCDATAAttribute("imageType", "metadata");
+        }
+        
+        attrs.addCDATAAttribute("grantAnyUser", String.valueOf(link.isAllowedAnyUser()));
+        
+        boolean isAccessLimited = link.isAllowedAnyUser() || ArrayUtils.isNotEmpty(link.getGrantedUsers()) || ArrayUtils.isNotEmpty(link.getGrantedGroups());
+        String isAccessLimitedStr = isAccessLimited ? _i18nUtils.translate(new I18nizableText("plugin.link-directory", "PLUGINS_LINKDIRECTORY_UITOOL_LIMITED_ACCESS_YES"))
+                                                    : _i18nUtils.translate(new I18nizableText("plugin.link-directory", "PLUGINS_LINKDIRECTORY_UITOOL_LIMITED_ACCESS_NO"));
+        
+        attrs.addCDATAAttribute("limitedAccess", isAccessLimitedStr); 
+        
+        XMLUtils.startElement(contentHandler, "link", attrs);
+
+        // Sax the access limitations if necessary
+        if (!saxLinksOnly)
+        {
+            _saxGrantedUsers(contentHandler, link);
+            _saxGrantedGroups(contentHandler, link);
+        }
+        
+        // Themes
+        if (saxThemesLabelOnly)
+        {
+            _saxThemesLabel(contentHandler, link);
+        }
+        else
+        {
+            _saxThemes(contentHandler, link);
+        }
+        
+        XMLUtils.endElement(contentHandler, "link");
     }
     
+    /**
+     * Get the actual ids of the configured themes
+     * @param configuredThemesNames the normalized ids of the configured themes
+     * @param siteName the site's name
+     * @param language the site's language
+     * @return the actual ids of the configured themes
+     */
+    public static Map<String, List<String>> getThemesMap(List<String> configuredThemesNames, String siteName, String language)
+    {
+        Map<String, List<String>> themesMap = new HashMap<String, List<String>> ();
+        List<String> correctThemesList = new ArrayList<String> ();
+        List<String> wrongThemesList = new ArrayList<String> ();
+        
+        for (int i = 0; i < configuredThemesNames.size(); i++)
+        {
+            ModifiableTraversableAmetysObject themesNode = DirectoryHelper.getThemesNode(_siteManager.getSite(siteName), language);
+            String configuredThemeName = configuredThemesNames.get(i);
+            
+            if (!themesNode.hasChild(configuredThemeName))
+            {
+                _logger.warn("The theme '" + configuredThemeName + "' was not found. It will be ignored.");
+                wrongThemesList.add(configuredThemeName);
+            }
+            else
+            {
+                AmetysObject configuredThemeNode = themesNode.getChild(configuredThemesNames.get(i));
+                correctThemesList.add(configuredThemeNode.getId());
+            }
+        }
+        
+        themesMap.put("correct-themes", correctThemesList);
+        themesMap.put("wrong-themes", wrongThemesList);
+        return themesMap;
+    }
+    
+    /**
+     * Verify the existence of a theme
+     * @param siteName the site's name
+     * @param language the site's language
+     * @param expression 
+     * @return true if the theme exists, false otherwise
+     */
+    public static boolean themeExists(String siteName, String language, Expression expression)
+    {
+        String xPathQuery = getThemeQuery(siteName, language, expression);
+        AmetysObjectIterable<AmetysObject> theme = _ametysObjectResolver.query(xPathQuery);
+        
+        return theme.hasNext();
+    }
+
+    /**
+     * Get the site's name
+     * @param request the request
+     * @return the site's name
+     */
+    public static String getSiteName(Request request)
+    {
+        Page page = (Page) request.getAttribute(Page.class.getName());
+        String siteName = null;
+        if (page != null)
+        {
+            siteName = page.getSiteName();
+        }
+        else
+        {
+            siteName = (String) request.getAttribute("site");
+        }
+        return siteName;
+    }
+
+    /**
+     * Get the site's language
+     * @param request the request
+     * @return the site's language
+     */
+    public static String getLanguage(Request request)
+    {
+        String language = (String) request.getAttribute("sitemapLanguage");
+        if (StringUtils.isEmpty(language))
+        {
+            language = request.getParameter("language");
+        }
+        
+        return language;
+    }
+    
+    /**
+     * Retrieve the context variables from the front
+     * @param request the request
+     * @return the map of context variables
+     */
+    public static Map<String, String> getContextVars(Request request)
+    {
+        Map<String, String> contextVars = new HashMap<String, String> ();
+        
+        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, getSiteName(request));
+        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, getLanguage(request));
+    
+        return contextVars;
+    }
+    
+    /**
+     * Sax the themes
+     * @param contentHandler the content handler 
+     * @param link the link 
+     * @throws SAXException If an error occurs while generating the SAX events
+     */
+    private static void _saxThemes (ContentHandler contentHandler, DefaultLink link) throws SAXException
+    {
+        XMLUtils.startElement(contentHandler, "themes");
+        
+        for (String themeId : link.getThemes())
+        {
+            try
+            {
+                DefaultTheme theme = _ametysObjectResolver.resolveById(themeId);
+                
+                AttributesImpl attrs = new AttributesImpl();
+                attrs.addCDATAAttribute("id", themeId);
+                attrs.addCDATAAttribute("label", theme.getLabel());
+                
+                XMLUtils.createElement(contentHandler, "theme", attrs);
+            }
+            catch (UnknownAmetysObjectException e)
+            {
+                // Theme does not exist anymore
+            }
+        }
+            
+        
+        XMLUtils.endElement(contentHandler, "themes");
+    }
+    
+    /**
+     * Sax the label of the themes
+     * @param contentHandler
+     * @param link
+     * @throws SAXException
+     */
+    private static void _saxThemesLabel (ContentHandler contentHandler, DefaultLink link) throws SAXException
+    {
+        List<String> themesLabel = new ArrayList<String>();
+        
+        for (String themeId : link.getThemes())
+        {
+            try
+            {
+                DefaultTheme theme = _ametysObjectResolver.resolveById(themeId);
+                themesLabel.add(theme.getLabel());
+            }
+            catch (UnknownAmetysObjectException e)
+            {
+                // Theme does not exist anymore
+            }
+        }
+        
+        XMLUtils.createElement(contentHandler, "themes", StringUtils.join(themesLabel, ", "));
+    }
+    
+    /**
+     * Sax the granted users
+     * @param contentHandler
+     * @param link
+     * @throws SAXException
+     */
+    private static void _saxGrantedUsers(ContentHandler contentHandler, DefaultLink link) throws SAXException
+    {
+        XMLUtils.startElement(contentHandler, "granted-users");
+        
+        for (String grantedUser : link.getGrantedUsers())
+        {
+            User user = _usersManager.getUser(grantedUser);
+            if (user != null)
+            {
+                AttributesImpl attrs = new AttributesImpl();
+                attrs.addCDATAAttribute("login", grantedUser);
+                attrs.addCDATAAttribute("fullName", user.getFullName());
+                XMLUtils.createElement(contentHandler, "granted-user", attrs);
+            }
+        }
+        XMLUtils.endElement(contentHandler, "granted-users");
+    }
+    
+    /**
+     * Sax the granted groups
+     * @param contentHandler
+     * @param link
+     * @throws SAXException
+     */
+    private static void _saxGrantedGroups(ContentHandler contentHandler, DefaultLink link) throws SAXException
+    {
+        XMLUtils.startElement(contentHandler, "granted-groups");
+        
+        for (String grantedGroupId : link.getGrantedGroups())
+        {
+            AttributesImpl attrs = new AttributesImpl();
+            attrs.addCDATAAttribute("id", grantedGroupId);
+            attrs.addCDATAAttribute("label", _groupsManager.getGroup(grantedGroupId).getLabel());
+            XMLUtils.createElement(contentHandler, "granted-group", attrs);
+
+        }
+        XMLUtils.endElement(contentHandler, "granted-groups");
+    }
+    
+    /**
+     * Determines if the current user is allowed to see the link or not
+     * @param link the link 
+     * @return true if the current user is allowed to see the link, false otherwise
+     */
+    private static boolean _isCurrentUserGrantedAccess(DefaultLink link)
+    {
+        RenderingContext renderingContext = _renderingContextHandler.getRenderingContext();
+        
+        // Any user can see all the links in the back-office or in preview mode regardless of the access limitations
+        if (!renderingContext.equals(RenderingContext.FRONT))
+        {
+            return true;
+        }
+        
+        Request request = ContextHelper.getRequest(_context);
+        String login = (String) request.getAttribute(WebConstants.FO_LOGIN);
+        
+        boolean isAllowedAnyUser = link.isAllowedAnyUser();
+        String[] grantedUsers = link.getGrantedUsers();
+        String[] grantedGroups = link.getGrantedGroups();
+        
+        // There is an access restriction on users or groups
+        if (ArrayUtils.isNotEmpty(grantedUsers) || ArrayUtils.isNotEmpty(grantedGroups))
+        {
+            return _isGrantedAccess(login, grantedUsers, grantedGroups);
+        }
+        
+        // There is an access restriction on connected users
+        if (isAllowedAnyUser)
+        {
+            return login != null ? true : false;
+        }
+        
+        // There is no access restriction
+        return true;
+    }
+    
+    /**
+     * Checks if the user has access to the page or not
+     * @param login the login of the user
+     * @param grantedUsers the list of granted users
+     * @param grantedGroups the list of granted groups
+     * @return true if the user is granted access, false otherwise
+     */
+    private static boolean _isGrantedAccess(String login, String[] grantedUsers, String[] grantedGroups)
+    {
+        Set<String> groups  = _groupsManager.getUserGroups(login);  
+
+        // Check the authorizations
+        if (ArrayUtils.contains(grantedUsers, login))
+        {
+            return true;
+        }
+        
+        for (String groupId : grantedGroups)
+        {
+            if (groups.contains(groupId))
+            {
+                return true;
+            }
+        }
+        
+        // The user doesn't belong to the granted users
+        return false;
+    }
 }
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/Link.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/Link.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/Link.java	(working copy)
@@ -17,15 +17,21 @@
 
 import java.io.InputStream;
 
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.lang.StringUtils;
+
+import org.ametys.plugins.repository.AmetysObject;
 import org.ametys.plugins.repository.AmetysRepositoryException;
 import org.ametys.plugins.repository.metadata.BinaryMetadata;
+import org.ametys.web.alias.DefaultAlias;
 
 /**
  * A link in the directory.
  */
 public interface Link
 {
-    
     /**
      * Get the URL.
      * @return the link URL.
@@ -147,7 +153,7 @@
     public void setPictureAlternative(String alternative) throws AmetysRepositoryException;
     
     /**
-     * Set the themes.
+     * Get the themes.
      * @return the themes in an Array
      * @throws AmetysRepositoryException if an error occurs.
      */
@@ -163,7 +169,49 @@
     /**
      * Remove theme if exist
      * @param themeId The id of theme to remove 
-     * @throws AmetysRepositoryException if an error occured
+     * @throws AmetysRepositoryException if an error occurs
      */
     public void removeTheme (String themeId) throws AmetysRepositoryException;
+    
+    /**
+     * Get the list of granted users
+     * @return the users who  have access to this link
+     * @throws AmetysRepositoryException if an error occurs
+     */
+    public String[] getGrantedUsers() throws AmetysRepositoryException;
+
+    /**
+     * Set the list of granted users
+     * @param grantedUsers the users who will have access to this link
+     * @throws AmetysRepositoryException if an error occurs
+     */
+    public void setGrantedUsers(String[] grantedUsers) throws AmetysRepositoryException;
+    
+    /**
+     * Get the list of granted groups
+     * @return excludedGroups the groups who don't have access to this link
+     * @throws AmetysRepositoryException if an error occurs
+     */
+    public String[] getGrantedGroups() throws AmetysRepositoryException;
+    
+    /**
+     * Set the list of granted groups 
+     * @param grantedGroups the groups who will have access to this link
+     * @throws AmetysRepositoryException if an exception occurs
+     */
+    public void setGrantedGroups(String[] grantedGroups) throws AmetysRepositoryException;
+    
+    /**
+     * Is any connected user allowed ?
+     * @return true if every connected user is allowed, false otherwise
+     * @throws AmetysRepositoryException if an exceptions occurs
+     */
+    public boolean isAllowedAnyUser() throws AmetysRepositoryException;
+    
+    /**
+     * Determines if any connected user will be granted access to this link, depending on the parameter's value
+     * @param allowAnyUser "true" to allow any connected user, "false" otherwise
+     * @throws AmetysRepositoryException
+     */
+    public void setGrantAnyUser(boolean allowAnyUser) throws AmetysRepositoryException;
 }
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/LinkDirectoryInputData.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/LinkDirectoryInputData.java	(revision 0)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/LinkDirectoryInputData.java	(revision 0)
@@ -0,0 +1,417 @@
+/*
+ *  Copyright 2015 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.plugins.linkdirectory;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.avalon.framework.activity.Initializable;
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
+import org.apache.avalon.framework.context.Context;
+import org.apache.avalon.framework.context.ContextException;
+import org.apache.avalon.framework.context.Contextualizable;
+import org.apache.avalon.framework.logger.AbstractLogEnabled;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.avalon.framework.service.Serviceable;
+import org.apache.cocoon.ProcessingException;
+import org.apache.cocoon.components.ContextHelper;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.xml.AttributesImpl;
+import org.apache.cocoon.xml.XMLUtils;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.excalibur.source.Source;
+import org.apache.excalibur.source.SourceResolver;
+import org.apache.tika.io.IOUtils;
+import org.xml.sax.ContentHandler;
+
+import org.ametys.plugins.linkdirectory.repository.DefaultLink;
+import org.ametys.web.WebConstants;
+import org.ametys.web.inputdata.InputData;
+import org.ametys.web.renderingcontext.RenderingContext;
+import org.ametys.web.renderingcontext.RenderingContextHandler;
+import org.ametys.web.repository.page.Page;
+import org.ametys.web.repository.site.Site;
+
+/**
+ * Input data for the link directory user preferences in thumbnails mode 
+ */
+public class LinkDirectoryInputData extends AbstractLogEnabled implements Contextualizable, InputData, Initializable, Serviceable
+{
+    /** The path to the configuration file */
+    private static final String __CONF_FILE_PATH = "skin://conf/link-themes.xml";
+    
+    /** The wildcard */
+    private static final String __WILDCARD = "*";
+    
+    /** The Avalon context */
+    private Context _context;
+    
+    /** The rendering context handler */
+    private RenderingContextHandler _renderingContextHandler;
+    
+    /** Excalibur source resolver */
+    private SourceResolver _sourceResolver;
+    
+    /** The configuration file */
+    private Map<String, Object> _configurationValues;
+
+    /** The last time the file was loaded */
+    private long _lastConfUpdate;
+
+    @Override
+    public void contextualize(Context context) throws ContextException
+    {
+        _context = context; 
+    }
+    
+    @Override
+    public void initialize() throws Exception
+    {
+        _lastConfUpdate = 0;
+    }
+    
+    @Override
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
+        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
+    }
+    
+    @Override
+    public boolean isCacheable(Site site, Page currentPage)
+    {
+        Request request = ContextHelper.getRequest(_context);
+        
+        if (_renderingContextHandler.getRenderingContext() != RenderingContext.FRONT)
+        {
+            // We're in the back or in preview
+            return true;
+        }
+        
+        String template = _getTemplate(request);
+        if (template == null)
+        {
+            return true;
+        }
+        
+        try
+        {
+            _updateConfigurationValues();
+            if (MapUtils.isEmpty(_configurationValues) || _configurationValues.get("error") != null)
+            {
+                // No configuration file or there are errors
+                return true;
+            }
+            
+            Map<String, Boolean> configurableAndApplicableMap = _isConfigurableAndApplicable(template);
+            if (!configurableAndApplicableMap.get("applicable"))
+            {
+                // The current template is not configured for a link directory input data
+                return true;
+            }
+            
+            if (configurableAndApplicableMap.get("configurable"))
+            {
+                // The applications are configurable
+                return false;
+            }
+            
+            // Find the selected theme ids for this template 
+            List<String> themesIds = _getConfiguredThemesNames(template);
+            
+            return !DirectoryHelper.hasRestrictions(DirectoryHelper.getSiteName(request), DirectoryHelper.getLanguage(request), themesIds);
+        }
+        catch (Exception e)
+        {
+            getLogger().error("An error occurred while retrieving information from the skin configuration", e);
+            // Configuration file is not readable => toSAX method will not generate any xml
+            return true;
+        }
+    }
+
+    @Override
+    public void toSAX(ContentHandler contentHandler) throws ProcessingException
+    {
+        Request request = ContextHelper.getRequest(_context);
+        
+        // Get the current user's login if he is in the front office
+        String login = null;
+        if (_renderingContextHandler.getRenderingContext().equals(RenderingContext.FRONT))
+        {
+            login = (String) request.getAttribute(WebConstants.FO_LOGIN);
+        }
+        
+        String template = _getTemplate(request);
+        if (template == null)
+        {
+            return; 
+        }
+        
+        try
+        {
+            _updateConfigurationValues();
+            if (MapUtils.isEmpty(_configurationValues))
+            {
+                return;
+            }
+
+            contentHandler.startDocument();
+            AttributesImpl attrs = new AttributesImpl();
+            
+            // Is there an error in the configuration file ?
+            String error = (String) _configurationValues.get("error");
+            if (error != null)
+            {
+                attrs.addCDATAAttribute("error", error);
+                XMLUtils.createElement(contentHandler, "linkDirectory", attrs);
+                return;
+            }
+            
+            Map<String, Boolean> isConfigurableAndApplicableMap = _isConfigurableAndApplicable(_getTemplate(request));
+            attrs.addCDATAAttribute("applicable", isConfigurableAndApplicableMap.get("applicable") ? "true" : "false");
+            attrs.addCDATAAttribute("configurable", isConfigurableAndApplicableMap.get("configurable") ? "true" : "false");
+            
+            XMLUtils.startElement(contentHandler, "linkDirectory", attrs);
+            
+            XMLUtils.startElement(contentHandler, "links");
+            
+            _saxThemesLinks(contentHandler, login, request);
+            
+            XMLUtils.endElement(contentHandler, "links");
+            
+            XMLUtils.endElement(contentHandler, "linkDirectory");
+            
+            contentHandler.endDocument();
+        }
+        catch (Exception e)
+        {
+            getLogger().error("An exception occurred during the processing of the link directory's input data" , e);
+        }
+    }
+
+    private void _saxThemesLinks(ContentHandler contentHandler, String login, Request request) throws ProcessingException
+    {
+        String language = DirectoryHelper.getLanguage(request);
+        String siteName = DirectoryHelper.getSiteName(request);
+        try
+        {
+            List<String> configuredThemesNames = _getConfiguredThemesNames(_getTemplate(request));
+            
+            // Get themes ids with their names
+            Map<String, List<String>> themesMap = DirectoryHelper.getThemesMap(configuredThemesNames, siteName, language);
+            List<String> correctThemesIds = (List<String>) themesMap.get("correct-themes");
+            List<String> wrongThemesNames = (List<String>) themesMap.get("wrong-themes");
+            
+            if (correctThemesIds.isEmpty() && wrongThemesNames.isEmpty())
+            { 
+                // Sax all links
+                List<DefaultLink> links = DirectoryHelper.getLinks(new ArrayList<String>(), login, siteName, language);
+
+                DirectoryHelper.saxLinks(contentHandler, links, DirectoryHelper.getContextVars(request), siteName + "/" + language, login, true, true);
+            }
+            else
+            {
+                    
+                if (!wrongThemesNames.isEmpty())
+                {
+                    // Sax the names of the wrong themes
+                    XMLUtils.startElement(contentHandler, "wrong-themes");
+                    for (String wrongThemeName : wrongThemesNames)
+                    {
+                        XMLUtils.createElement(contentHandler, "wrong-theme", wrongThemeName);
+                    }
+                    XMLUtils.endElement(contentHandler, "wrong-themes");
+                }
+                
+                
+                if (correctThemesIds.isEmpty())
+                {
+                    getLogger().warn("All the themes ids are mispelled or the corresponding themes do not exist anymore.");
+                    return;
+                }
+                
+                // Get the links corresponding to the correct configured themes names
+                List<DefaultLink> links = DirectoryHelper.getLinks(correctThemesIds, login, siteName, language);
+                
+                // Sax the final set of links
+                DirectoryHelper.saxLinks(contentHandler, links, DirectoryHelper.getContextVars(request), siteName + "/" + language, login, true, true);
+            }
+        }
+        catch (Exception e)
+        {
+            throw new ProcessingException("An error occurred while retrieving or saxing the links", e);
+        }
+    }
+    
+    /**
+     * Retrieve the configured themes names defined in the skin file link-themes.xml for the current template
+     * @param template the current page's template
+     * @return the list of configured themes ids
+     */
+    private List<String> _getConfiguredThemesNames(String template)
+    {
+        List<Map<String, Object>> themesList = (List<Map<String, Object>>) _configurationValues.get("themes");
+        
+        for (Map<String, Object> themesMap : themesList)
+        {
+            List<String> templates = (List<String>) themesMap.get("templates");
+            if (templates.contains(template) || templates.contains(__WILDCARD))
+            {
+                // Get first matching template
+                return (List<String>) themesMap.get("theme-list");
+            }    
+        }
+        
+        return new ArrayList<String>();
+    }
+    
+    /**
+     * Retrieve the "configurable" attribute of a template of the configuration file
+     * @param template the current page's template
+     * @return true if the template is configurable, false otherwise
+     */
+    private Map<String, Boolean> _isConfigurableAndApplicable(String template) 
+    {
+        Map<String, Boolean> configurableAndApplicableMap = new HashMap<String, Boolean> ();
+        configurableAndApplicableMap.put("applicable", false);
+        configurableAndApplicableMap.put("configurable", false);
+        
+        List<Map<String, Object>> themesList = (List<Map<String, Object>>) _configurationValues.get("themes");
+        for (Map<String, Object> themesMap : themesList)
+        {
+            List<String> templates = (List<String>) themesMap.get("templates");
+            if (templates.contains(template) || templates.contains(__WILDCARD))
+            {
+                configurableAndApplicableMap.put("applicable", true);
+                configurableAndApplicableMap.put("configurable", (Boolean) themesMap.get("configurable"));
+                
+                break;
+            }
+        }
+        
+        return configurableAndApplicableMap;
+    }
+    
+    /**
+     * Update the configuration values : read them if the map is empty, update them if the file was changed or simply return them
+     * @throws Exception if an exception occurs
+     */
+    private void _updateConfigurationValues() throws Exception
+    {
+        Source source = null;
+        try
+        {
+            source = _sourceResolver.resolveURI(__CONF_FILE_PATH);
+            if (!source.exists())
+            {
+                getLogger().error("There is no configuration file at path '" + __CONF_FILE_PATH + "'.");
+            }
+            
+            long lastModified = source.getLastModified();
+            if (_lastConfUpdate != 0)
+            {
+                if (lastModified == _lastConfUpdate)
+                {
+                    return;
+                }
+            }
+            
+            _lastConfUpdate = lastModified;
+            _cacheConfigurationValues(source);
+        }
+        finally
+        {
+            if (_sourceResolver != null && source != null)
+            {
+                _sourceResolver.release(source);
+            }
+        }
+    }
+    
+    /**
+     * Read the configuration values and store them
+     * @param source the file's source
+     * @throws Exception if an exception occurs
+     */
+    private void _cacheConfigurationValues (Source source)
+    {
+        Map<String, Object> configurationValues = new HashMap<String, Object> ();
+        InputStream is = null;
+        
+        try 
+        {
+            is = source.getInputStream();
+            Configuration configuration = new DefaultConfigurationBuilder().build(source.getInputStream());
+            
+            Configuration[] themesConfigurations = configuration.getChildren("themes");
+            
+            List<Map<String, Object>> themesList = new ArrayList<Map<String, Object>> ();
+            for (Configuration themesConfiguration : themesConfigurations)
+            {
+                Map<String, Object> themesMap = new HashMap<String, Object> ();
+                List<String> themeList = new ArrayList<String> ();
+                
+                Configuration[] themeConfigurations = themesConfiguration.getChildren();
+                for (Configuration themeConfiguration : themeConfigurations)
+                {
+                    themeList.add(themeConfiguration.getAttribute("id", null));
+                }
+                
+                String templatesStr = themesConfiguration.getAttribute("templates", "*");
+                String[] templatesList = StringUtils.split(templatesStr, ',');
+                
+                themesMap.put("templates", Arrays.asList(templatesList));
+                themesMap.put("theme-list", themeList);
+                themesMap.put("configurable", themesConfiguration.getAttributeAsBoolean("configurable", false));
+                
+                themesList.add(themesMap);
+            }
+            
+            configurationValues.put("themes", themesList);
+            
+            _configurationValues = configurationValues;
+        }
+        catch (Exception e)
+        {
+            Map<String, Object> error = new HashMap<String, Object>();
+            getLogger().warn("An error occured while getting the configuration's file values", e);
+            error.put("error", e.getMessage());
+            
+            _configurationValues = error;
+        }
+        finally
+        {
+            IOUtils.closeQuietly(is);
+        }
+    }
+       
+    /**
+     * Get the current template 
+     * @param request the request
+     * @return the current template
+     */
+    private String _getTemplate(Request request)
+    {
+        return (String) request.getAttribute("template");
+    }
+}
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/LinkDirectoryService.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/LinkDirectoryService.java	(revision 0)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/LinkDirectoryService.java	(revision 0)
@@ -0,0 +1,58 @@
+/*
+ *  Copyright 2015 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.plugins.linkdirectory;
+
+import java.util.Arrays;
+
+import org.apache.commons.lang.StringUtils;
+
+import org.ametys.plugins.repository.metadata.CompositeMetadata;
+import org.ametys.web.repository.page.Page;
+import org.ametys.web.repository.page.ZoneItem;
+
+/**
+ *  Service for the link directory : not cacheable when dealing with links that have a limited access
+ */
+public class LinkDirectoryService extends org.ametys.web.service.StaticService
+{
+    @Override
+    public boolean isCacheable(Page currentPage, ZoneItem zoneItem)
+    {
+        CompositeMetadata serviceParameters = zoneItem.getServiceParameters();
+        boolean configurable = serviceParameters.getBoolean("configurable");
+        
+        if (configurable)
+        {
+            return false;
+        }
+        
+        if (currentPage != null)
+        {
+            // Get the selected themes ids from the service parameters
+            String themesIdsStr = serviceParameters.getString("themes");
+            String[] themesIds = StringUtils.split(themesIdsStr, ',');
+            
+            // Get the site name and the language from the page
+            String siteName = currentPage.getSiteName();
+            String language = currentPage.getSitemapName();
+            
+            return !DirectoryHelper.hasRestrictions(siteName, language, Arrays.asList(themesIds));
+        }
+
+        // Current page is null
+        return true;
+    }
+}
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/LinkDirectorySetUserPreferencesAction.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/LinkDirectorySetUserPreferencesAction.java	(revision 0)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/LinkDirectorySetUserPreferencesAction.java	(revision 0)
@@ -0,0 +1,138 @@
+/*
+ *  Copyright 2015 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.plugins.linkdirectory;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.avalon.framework.parameters.Parameters;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.acting.ServiceableAction;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Redirector;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.environment.SourceResolver;
+import org.apache.commons.lang.StringUtils;
+
+import org.ametys.runtime.plugins.core.userpref.UserPreferencesManager;
+import org.ametys.runtime.util.cocoon.ActionResultGenerator;
+import org.ametys.web.WebConstants;
+import org.ametys.web.renderingcontext.RenderingContext;
+import org.ametys.web.renderingcontext.RenderingContextHandler;
+import org.ametys.web.userpref.FOUserPreferencesConstants;
+
+/**
+ * Create or set user preferences attached to a link directory service or the link directory input data mode for front-end users
+ */
+public class LinkDirectorySetUserPreferencesAction extends ServiceableAction
+{
+    /** The user preferences manager. */
+    private UserPreferencesManager _userPrefManager;
+
+    /** The rendering context handler */
+    private RenderingContextHandler _renderingContextHandler;
+    
+    @Override
+    public void service(ServiceManager serviceManager) throws ServiceException
+    {
+        super.service(serviceManager);
+        _userPrefManager = (UserPreferencesManager) serviceManager.lookup(UserPreferencesManager.ROLE + ".FO");
+        _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
+    }
+    
+    @Override
+    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
+    {
+        Map<String, Object> result = new HashMap<String, Object>();
+        Request request = ObjectModelHelper.getRequest(objectModel);
+        
+        // We save user preferences for the front-end users only
+        if (_renderingContextHandler.getRenderingContext().equals(RenderingContext.FRONT))
+        {
+            String login = (String) request.getAttribute(WebConstants.FO_LOGIN);
+            if (StringUtils.isNotEmpty(login))
+            {
+                Map<String, String> values = new HashMap<String, String>();
+                
+                String siteName = DirectoryHelper.getSiteName(request);
+                String language = DirectoryHelper.getLanguage(request);
+                String zoneItemId = request.getParameter("zone-item-id");
+                
+                String storageContext = _getStorageContext(siteName, language, zoneItemId);
+                Map<String, String> contextVars = _getContextVars(siteName, language);
+                
+                values.put("checked-links", _getCheckedLinks(request));
+                
+                _userPrefManager.setUserPreferences(login, storageContext, contextVars, values);
+                
+                request.setAttribute(ActionResultGenerator.MAP_REQUEST_ATTR, result);
+            }
+        }
+        
+        return EMPTY_MAP;
+    }
+
+    /**
+     * Get the appropriate storage context 
+     * @param siteName the name of the site
+     * @param language the language
+     * @param zoneItemId the id of the zone item if we deal with a service, null for an input data
+     * @return the storage context in which the user preferences will be kept
+     */
+    private String _getStorageContext(String siteName, String language, String zoneItemId)
+    {
+        return StringUtils.isEmpty(zoneItemId) ? siteName + "/" + language : siteName + "/" + language + "/" + zoneItemId;
+    }
+    
+    /**
+     * Get the checked link ids from the request
+     * @param request the request
+     * @return the comma-separated string of checked links ids
+     */
+    private String _getCheckedLinks(Request request)
+    {
+        List<String> checkedLinksIds = new ArrayList<String> ();
+        Enumeration<String> parameterNames = request.getParameterNames();
+        while (parameterNames.hasMoreElements())
+        {
+            String parameterName = parameterNames.nextElement();
+            if (parameterName.startsWith("checkbox-"))
+            {
+                String isChecked = request.getParameter(parameterName);
+                if (isChecked.equals("true"))
+                {
+                    checkedLinksIds.add(parameterName.substring(9));
+                }
+            }
+        }
+        
+        return StringUtils.join(checkedLinksIds, ',');
+    }
+    
+    private Map<String, String> _getContextVars(String siteName, String language)
+    {
+        Map<String, String> contextVars = new HashMap<String, String>();
+        
+        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, siteName);
+        contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, language);
+        
+        return contextVars;
+    }
+}
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/CreateLinkAction.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/CreateLinkAction.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/CreateLinkAction.java	(working copy)
@@ -95,6 +95,12 @@
         String pictureType = (String) jsParameters.get("picture#type");
         String pictureAlternative = StringUtils.defaultString((String) jsParameters.get("picture-alternative"));
         String themesStr = (String) jsParameters.get("themes");
+        boolean grantAnyUser = (Boolean) jsParameters.get("grant-any-user");
+        String foUsers = (String) jsParameters.get("fousers");
+        String foGroups = (String) jsParameters.get("fogroups");
+
+        String[] grantedUsers = StringUtils.split(foUsers, ',');
+        String[] grantedGroups = StringUtils.split(foGroups, ',');
         
         // Check that the word doesn't already exist.
         if (urlExists(url, siteName, language))
@@ -113,7 +119,7 @@
         link.setAlternative(alternative);
         link.setPictureAlternative(pictureAlternative);
         link.setThemes(StringUtils.split(themesStr, ','));
-        
+
         if (StringUtils.isEmpty(pictureId))
         {
             // Deletion.
@@ -135,6 +141,10 @@
             link.setExternalPicture(finalMimeType, filename, upload.getInputStream());
         }
         
+        link.setGrantAnyUser(grantAnyUser);
+        link.setGrantedUsers(grantedUsers);
+        link.setGrantedGroups(grantedGroups);
+        
         rootNode.saveChanges();
         
         result.put("id", link.getId());
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/LinkGenerator.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/LinkGenerator.java	(revision 29005)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/LinkGenerator.java	(working copy)
@@ -1,5 +1,5 @@
 /*
- *  Copyright 2012 Anyware Services
+ *  Copyright 2015 Anyware Services
  *
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -16,8 +16,6 @@
 package org.ametys.plugins.linkdirectory.link;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
@@ -25,17 +23,11 @@
 import org.apache.cocoon.environment.ObjectModelHelper;
 import org.apache.cocoon.environment.Request;
 import org.apache.cocoon.generation.ServiceableGenerator;
-import org.apache.cocoon.xml.AttributesImpl;
-import org.apache.cocoon.xml.XMLUtils;
-import org.apache.commons.lang.StringUtils;
 import org.xml.sax.SAXException;
 
-import org.ametys.plugins.explorer.resources.Resource;
+import org.ametys.plugins.linkdirectory.DirectoryHelper;
 import org.ametys.plugins.linkdirectory.repository.DefaultLink;
-import org.ametys.plugins.linkdirectory.repository.DefaultTheme;
 import org.ametys.plugins.repository.AmetysObjectResolver;
-import org.ametys.plugins.repository.UnknownAmetysObjectException;
-import org.ametys.plugins.repository.metadata.BinaryMetadata;
 
 /**
  * Generate a directory link as XML.
@@ -63,109 +55,8 @@
         contentHandler.startDocument();
         
         DefaultLink link = _resolver.resolveById(id);
-        saxLink(link, themeLabelOnly);
+        DirectoryHelper.saxLink(contentHandler, link, themeLabelOnly, false, false);
         
         contentHandler.endDocument();
     }
-    
-    /**
-     * Generate a directory link.
-     * @param link the link to generate.
-     * @param saxThemesLabelOnly true to sax themes label only
-     * @throws SAXException If an error occurs while generating the XML.
-     */
-    protected void saxLink (DefaultLink link, boolean saxThemesLabelOnly) throws SAXException
-    {
-        AttributesImpl attrs = new AttributesImpl();
-        attrs.addCDATAAttribute("id", link.getId());
-        
-        attrs.addCDATAAttribute("url", StringUtils.defaultString(link.getUrl()));
-        attrs.addCDATAAttribute("title", StringUtils.defaultString(link.getTitle()));
-        attrs.addCDATAAttribute("content", StringUtils.defaultString(link.getContent()));
-        
-        attrs.addCDATAAttribute("alternative", StringUtils.defaultString(link.getAlternative()));
-        attrs.addCDATAAttribute("pictureAlternative", StringUtils.defaultString(link.getPictureAlternative()));
-        
-        String pictureType = link.getPictureType();
-        attrs.addCDATAAttribute("pictureType", pictureType);
-        if (pictureType.equals("resource"))
-        {
-            String resourceId = link.getResourcePictureId();
-            Resource resource = _resolver.resolveById(resourceId);
-            attrs.addCDATAAttribute("pictureId", resourceId);
-            attrs.addCDATAAttribute("pictureName", resource.getName());
-            attrs.addCDATAAttribute("pictureSize", Long.toString(resource.getLength()));
-            attrs.addCDATAAttribute("imageType", "explorer");
-        }
-        else if (pictureType.equals("external"))
-        {
-            BinaryMetadata picMeta = link.getExternalPicture();
-            attrs.addCDATAAttribute("picturePath", DefaultLink.METADATA_PICTURE);
-            attrs.addCDATAAttribute("pictureName", picMeta.getFilename());
-            attrs.addCDATAAttribute("pictureSize", Long.toString(picMeta.getLength()));
-            attrs.addCDATAAttribute("imageType", "metadata");
-        }
-        
-        XMLUtils.startElement(contentHandler, "link", attrs);
-        
-        // Themes.
-        if (saxThemesLabelOnly)
-        {
-            _saxThemesLabel(link);
-        }
-        else
-        {
-            _saxThemes(link);
-        }
-        
-        XMLUtils.endElement(contentHandler, "link");
-    }
-    
-    private void _saxThemes (DefaultLink link) throws SAXException
-    {
-        XMLUtils.startElement(contentHandler, "themes");
-        
-        for (String themeId : link.getThemes())
-        {
-            try
-            {
-                DefaultTheme theme = _resolver.resolveById(themeId);
-                
-                AttributesImpl attrs = new AttributesImpl();
-                attrs.addCDATAAttribute("id", themeId);
-                attrs.addCDATAAttribute("label", theme.getLabel());
-                
-                XMLUtils.createElement(contentHandler, "theme", attrs);
-            }
-            catch (UnknownAmetysObjectException e)
-            {
-                // Theme does not exist anymore
-            }
-        }
-            
-        
-        XMLUtils.endElement(contentHandler, "themes");
-    }
-    
-    
-    private void _saxThemesLabel (DefaultLink link) throws SAXException
-    {
-        List<String> themesLabel = new ArrayList<String>();
-        
-        for (String themeId : link.getThemes())
-        {
-            try
-            {
-                DefaultTheme theme = _resolver.resolveById(themeId);
-                themesLabel.add(theme.getLabel());
-            }
-            catch (UnknownAmetysObjectException e)
-            {
-                // Theme does not exist anymore
-            }
-        }
-        
-        XMLUtils.createElement(contentHandler, "themes", StringUtils.join(themesLabel, ", "));
-    }
-
 }
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/LinkListGenerator.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/LinkListGenerator.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/LinkListGenerator.java	(working copy)
@@ -1,5 +1,5 @@
 /*
- *  Copyright 2012 Anyware Services
+ *  Copyright 2015 Anyware Services
  *
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -16,49 +16,57 @@
 package org.ametys.plugins.linkdirectory.link;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
 import org.apache.cocoon.ProcessingException;
 import org.apache.cocoon.environment.ObjectModelHelper;
 import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.generation.ServiceableGenerator;
 import org.apache.cocoon.xml.AttributesImpl;
 import org.apache.cocoon.xml.XMLUtils;
+import org.apache.commons.lang.ArrayUtils;
 import org.apache.commons.lang.StringUtils;
 import org.xml.sax.SAXException;
 
 import org.ametys.plugins.linkdirectory.DirectoryHelper;
 import org.ametys.plugins.linkdirectory.repository.DefaultLink;
-import org.ametys.plugins.repository.AmetysObjectIterable;
-import org.ametys.plugins.repository.TraversableAmetysObject;
-import org.ametys.plugins.repository.query.expression.Expression;
-import org.ametys.plugins.repository.query.expression.OrExpression;
+import org.ametys.plugins.linkdirectory.theme.ThemeExpression;
+import org.ametys.web.WebConstants;
+import org.ametys.web.renderingcontext.RenderingContext;
+import org.ametys.web.renderingcontext.RenderingContextHandler;
 import org.ametys.web.repository.page.Page;
-import org.ametys.web.repository.site.Site;
-import org.ametys.web.repository.site.SiteManager;
+import org.ametys.web.repository.page.ZoneItem;
 
 /**
- * Generate the list of directory links as XML.
+ * Generate the links' list of a link directory as XML.
  */
-public class LinkListGenerator extends LinkGenerator
+public class LinkListGenerator extends ServiceableGenerator 
 {
-    private SiteManager _siteManager;
+    /** The rendering context handler */
+    private RenderingContextHandler _renderingContextHandler;
     
     @Override
     public void service(ServiceManager serviceManager) throws ServiceException
     {
         super.service(serviceManager);
-        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
+        _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
     }
     
     @Override
     public void generate() throws IOException, SAXException, ProcessingException
     {
         Request request = ObjectModelHelper.getRequest(objectModel);
+        
+        Page page = (Page) request.getAttribute(Page.class.getName());
+
         String siteName = parameters.getParameter("siteName", (String) request.getAttribute("site"));
         String language = parameters.getParameter("language", (String) request.getAttribute("renderingLanguage"));
         String themes = parameters.getParameter("themes", request.getParameter("themes"));
-
+        
         boolean boMode = "true".equals(request.getParameter("bo-mode"));
         
         if (StringUtils.isEmpty(siteName))
@@ -69,11 +77,7 @@
         {
             language = request.getParameter("language");
         }
-        
-        Page page = (Page) request.getAttribute(Page.class.getName());
-        
-        Site site = _siteManager.getSite(siteName);
-        
+
         contentHandler.startDocument();
         
         AttributesImpl attrs = new AttributesImpl();
@@ -83,58 +87,74 @@
         {
             attrs.addCDATAAttribute("page", page.getId());
         }
-        XMLUtils.startElement(contentHandler, "links", attrs);
         
-        AmetysObjectIterable<DefaultLink> links = getLinks(site, language, themes);
-        
-        try
+        String login = null;
+        if (_renderingContextHandler.getRenderingContext().equals(RenderingContext.FRONT))
         {
-            for (DefaultLink link : links)
-            {
-                saxLink(link, boMode);
-            }
+            login = (String) request.getAttribute(WebConstants.FO_LOGIN);
         }
-        finally
+       
+        // Get the themes as a list
+        String[] themesArray = StringUtils.split(themes, ",");
+        List<String> themesIdsList = ArrayUtils.isEmpty(themesArray) ? new ArrayList<String> () : Arrays.asList(themesArray);
+        
+        List<DefaultLink> links = DirectoryHelper.getLinks(themesIdsList, login, siteName, language);
+        
+        String storageContext = null;
+        
+        // Get the zone item if we sax for a service
+        if (!boMode)
         {
-            if (links != null)
+            ZoneItem zoneItem = (ZoneItem) request.getAttribute(ZoneItem.class.getName());
+            attrs.addCDATAAttribute("zoneItemId", zoneItem.getId());
+            
+            if (login != null)
             {
-                links.close();
+                storageContext = DirectoryHelper.getSiteName(request) + "/" + DirectoryHelper.getLanguage(request) + "/" + zoneItem.getId();
             }
         }
         
+        // SAX the links
+        XMLUtils.startElement(contentHandler, "links", attrs);
+        try
+        {
+            // Sax the amount of deleted themes 
+            _saxDeletedThemes(siteName, language, themesIdsList);
+            
+            DirectoryHelper.saxLinks(contentHandler, links, DirectoryHelper.getContextVars(request), storageContext, login, boMode, true);
+        }
+        catch (Exception e)
+        {
+            getLogger().error("An exception occurred while saxing the links", e);
+        }
         XMLUtils.endElement(contentHandler, "links");
+        
         contentHandler.endDocument();
     }
 
     /**
-     * Get the links.
-     * @param site the site.
-     * @param language the language.
-     * @param themes the themes or null.
-     * @return the links.
+     * Sax the amount of deleted themes
+     * @param siteName the site's name
+     * @param language the language
+     * @param themesIdsList the list of theme ids
+     * @throws SAXException if an exception occurs while saxing
      */
-    protected AmetysObjectIterable<DefaultLink> getLinks(Site site, String language, String themes)
+    protected void _saxDeletedThemes(String siteName, String language, List<String> themesIdsList) throws SAXException
     {
-        AmetysObjectIterable<DefaultLink> links;
-        if (StringUtils.isEmpty(themes))
+        int amount = 0;
+        for (String themeId : themesIdsList)
         {
-            TraversableAmetysObject linksNode = DirectoryHelper.getLinksNode(site, language);
-            links = linksNode.getChildren();
-        }
-        else
-        {
-            String[] themeArr = StringUtils.split(themes, ',');
-            Expression[] expressions = new Expression[themeArr.length];
-            
-            for (int i = 0; i < themeArr.length; i++)
+            if (!DirectoryHelper.themeExists(siteName, language, new ThemeExpression(themeId)))
             {
-                expressions[i] = new ThemeExpression(themeArr[i]);
+                // The theme was deleted but it is still referenced by the service
+                getLogger().warn("The id '" + themeId + "' does not reference an existing theme.");
+                amount += 1;
             }
-            
-            String query = DirectoryHelper.getLinkQuery(site.getName(), language, new OrExpression(expressions));
-            
-            links = _resolver.query(query);
+        }
+        
+        if (amount > 0)
+        {
+            XMLUtils.createElement(contentHandler, "deleted-themes", Integer.toString(amount));
         }
-        return links;
     }
 }
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/MoveLinkAction.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/MoveLinkAction.java	(revision 0)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/MoveLinkAction.java	(revision 0)
@@ -0,0 +1,169 @@
+/*
+ *  Copyright 2015 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.plugins.linkdirectory.link;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.avalon.framework.parameters.Parameters;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.acting.ServiceableAction;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Redirector;
+import org.apache.cocoon.environment.SourceResolver;
+
+import org.ametys.plugins.linkdirectory.repository.DefaultLink;
+import org.ametys.plugins.repository.AmetysObject;
+import org.ametys.plugins.repository.AmetysObjectIterable;
+import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.plugins.repository.ModifiableAmetysObject;
+import org.ametys.plugins.repository.TraversableAmetysObject;
+
+/**
+ * Action that moves a link in JCR
+ */
+public class MoveLinkAction extends ServiceableAction
+{
+    /** The Ametys object resolver */
+    protected AmetysObjectResolver _resolver;
+    
+    @Override
+    public void service(ServiceManager serviceManager) throws ServiceException
+    {
+        super.service(serviceManager);
+        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
+    }
+    
+    @Override
+    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
+    {
+        Map<String, String> result = new HashMap<String, String>();
+        Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
+        
+        String id = parameters.getParameter("id", (String) jsParameters.get("id"));
+        String role = parameters.getParameter("role", (String) jsParameters.get("role"));
+        
+        DefaultLink link = _resolver.resolveById(id);
+        
+        if ("move-first".equals(role))
+        {
+            _moveFirst(link);
+        }
+        else if ("move-up".equals(role))
+        {
+            _moveUp(link);
+        }
+        else if ("move-down".equals(role))
+        {
+            _moveDown(link);
+        }
+        else if ("move-last".equals(role))
+        {
+            _moveLast(link);
+        }
+        
+        return result;
+    }
+    
+    /**
+     * Move first.
+     * @param link 
+     * @throws RepositoryException
+     */
+    private void _moveFirst(DefaultLink link) throws RepositoryException
+    {
+        AmetysObjectIterable<AmetysObject>  children = null;
+        try
+        {
+            children = ((TraversableAmetysObject) link.getParent()).getChildren();
+            // Resolve the link in the same session or the linkRoot.saveChanges() call below won't see the order changes.
+            link.orderBefore(((TraversableAmetysObject) link.getParent()).getChildren().next());
+            ((ModifiableAmetysObject) link.getParent()).saveChanges();
+        }
+        finally
+        {
+            if (children != null)
+            {
+                children.close();
+            }
+        }
+    }
+
+    /**
+     * Move down.
+     * @param link 
+     * @throws RepositoryException
+     */
+    private void _moveDown(DefaultLink link) throws RepositoryException
+    {
+        TraversableAmetysObject parentNode = link.getParent();
+        AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();
+        
+        boolean iterate = true;
+        while (siblings.hasNext() && iterate)
+        {
+            DefaultLink sibling = (DefaultLink) siblings.next();
+            iterate = !sibling.getName().equals(link.getName());
+        }
+        
+        // Move the link after his next sibling: move the next sibling before the link to move.
+        DefaultLink nextLink = (DefaultLink) siblings.next();
+        nextLink.orderBefore(link);
+
+        link.saveChanges();
+    }
+    
+    /**
+     * Move up.
+     * @param link
+     * @throws RepositoryException
+     */
+    private void _moveUp(DefaultLink link) throws RepositoryException
+    {
+        TraversableAmetysObject parentNode = link.getParent();
+        AmetysObjectIterable<AmetysObject> siblings = parentNode.getChildren();
+        DefaultLink previousLink = null;
+
+        while (siblings.hasNext())
+        {
+            DefaultLink sibling = (DefaultLink) siblings.next();
+            if (sibling.getName().equals(link.getName()))
+            {
+                break;
+            }
+            
+            previousLink = sibling;
+        }
+            
+        // Move the link after his next sibling: move the next sibling before the link to move.
+        link.orderBefore(previousLink);
+        link.saveChanges();
+    }
+    
+    /**
+     * Move last.
+     * @param link 
+     * @throws RepositoryException
+     */
+    private void _moveLast(DefaultLink link) throws RepositoryException
+    {
+        link.moveTo(link.getParent(), false);
+        ((ModifiableAmetysObject) link.getParent()).saveChanges();
+    }
+}
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/ThemeExpression.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/ThemeExpression.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/ThemeExpression.java	(working copy)
@@ -1,49 +0,0 @@
-/*
- *  Copyright 2011 Anyware Services
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.ametys.plugins.linkdirectory.link;
-
-import org.ametys.plugins.linkdirectory.repository.DefaultLink;
-import org.ametys.plugins.repository.query.expression.Expression;
-
-/**
- * Build a {@link Expression} for links themes
- *
- */
-public class ThemeExpression implements Expression
-{
-    
-    /** The theme label. */
-    protected String _value;
-    
-    /**
-     * Build a ThemeExpression from a label.
-     * @param value the theme label.
-     */
-    public ThemeExpression(String value)
-    {
-        _value = value;
-    }
-    
-    @Override
-    public String build()
-    {
-        StringBuilder buff = new StringBuilder();
-        
-        buff.append('@').append(DefaultLink.METADATA_THEMES).append(Operator.EQ).append("'").append(_value).append("'");
-        
-        return buff.toString();
-    }
-}
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/UpdateLinkAction.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/UpdateLinkAction.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/link/UpdateLinkAction.java	(working copy)
@@ -55,6 +55,13 @@
         String pictureType = (String) jsParameters.get("picture#type");
         String pictureAlternative = StringUtils.defaultString((String) jsParameters.get("picture-alternative"));
         String themesStr = (String) jsParameters.get("themes");
+        boolean grantAnyUser = (Boolean) jsParameters.get("grant-any-user");
+        
+        String foUsers = (String) jsParameters.get("fousers");
+        String foGroups = (String) jsParameters.get("fogroups");
+        
+        String[] grantedUsers = StringUtils.split(foUsers, ',');
+        String[] grantedGroups = StringUtils.split(foGroups, ',');
         
         try
         {
@@ -97,6 +104,10 @@
                 link.setExternalPicture(finalMimeType, filename, upload.getInputStream());
             }
             
+            link.setGrantAnyUser(grantAnyUser); 
+            link.setGrantedUsers(grantedUsers);
+            link.setGrantedGroups(grantedGroups);
+            
             link.saveChanges();
             
             result.put("id", link.getId());
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/repository/DefaultLink.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/repository/DefaultLink.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/repository/DefaultLink.java	(working copy)
@@ -24,11 +24,14 @@
 import javax.jcr.Value;
 
 import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
 
 import org.ametys.plugins.linkdirectory.Link;
 import org.ametys.plugins.repository.AmetysObject;
 import org.ametys.plugins.repository.AmetysRepositoryException;
+import org.ametys.plugins.repository.MovableAmetysObject;
 import org.ametys.plugins.repository.RepositoryConstants;
+import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
 import org.ametys.plugins.repository.jcr.SimpleAmetysObject;
 import org.ametys.plugins.repository.metadata.BinaryMetadata;
 import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata;
@@ -39,7 +42,7 @@
 /**
  * Repository implementation of a directory link.
  */
-public class DefaultLink extends SimpleAmetysObject<DefaultLinkFactory> implements Link, SiteAwareAmetysObject
+public class DefaultLink extends SimpleAmetysObject<DefaultLinkFactory> implements Link, SiteAwareAmetysObject, MovableAmetysObject
 {
     
     /** Constant for URL metadata. */
@@ -60,6 +63,12 @@
     public static final String METADATA_PICTURE_ALTERNATIVE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":picture-alternative";
     /** Constant for themes metadata. */
     public static final String METADATA_THEMES = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":themes";
+    /** Constant for grant-any-user metadata. */
+    public static final String METADATA_GRANT_ANY_USER = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":grant-any-user";
+    /** Constant for granted-users metadata. */
+    public static final String METADATA_GRANTED_USERS = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":granted-users";
+    /** Constant for granted-groups metadata. */
+    public static final String METADATA_GRANTED_GROUPS = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":granted-groups";
     
     /**
      * Create a {@link DefaultLink}.
@@ -330,57 +339,78 @@
     @Override
     public String[] getThemes() throws AmetysRepositoryException
     {
+        return _getListFromMetadataName(METADATA_THEMES);
+    }
+
+    @Override
+    public void setThemes(String[] themes) throws AmetysRepositoryException
+    {
+        _setListWithMetadataName(themes, METADATA_THEMES);
+    }
+
+    @Override
+    public void removeTheme (String themeId) throws AmetysRepositoryException
+    {
         try
         {
-            if (getNode().hasProperty(METADATA_THEMES))
-            {
-                Value[] values = getNode().getProperty(METADATA_THEMES).getValues();
-                
-                String[] themes = new String[values.length];
-                
-                for (int i = 0; i < values.length; i++)
-                {
-                    themes[i] = values[i].getString();
-                }
-                
-                return themes;
-            }
-            else
-            {
-                return new String[0];
-            }
+            String[] themes = getThemes();
+            String[] updatedThemes = (String[]) ArrayUtils.removeElement(themes, themeId);
+            getNode().setProperty(METADATA_THEMES, updatedThemes);
         }
         catch (RepositoryException e)
         {
-            throw new AmetysRepositoryException("Error getting the themes property.", e);
+            throw new AmetysRepositoryException("Error removing theme of id " + themeId, e);
         }
     }
     
     @Override
-    public void setThemes(String[] themes) throws AmetysRepositoryException
+    public String[] getGrantedUsers() throws AmetysRepositoryException
+    {
+        return _getListFromMetadataName(METADATA_GRANTED_USERS);
+    }
+    
+    @Override
+    public void setGrantedUsers(String[] grantedUsers) throws AmetysRepositoryException
+    {
+        _setListWithMetadataName(grantedUsers, METADATA_GRANTED_USERS);
+    }
+    
+    @Override
+    public String[] getGrantedGroups() throws AmetysRepositoryException
     {
+        return _getListFromMetadataName(METADATA_GRANTED_GROUPS);
+    }
+    
+    @Override
+    public void setGrantedGroups(String[] grantedGroups) throws AmetysRepositoryException
+    {
+        _setListWithMetadataName(grantedGroups, METADATA_GRANTED_GROUPS);
+    }
+    
+    @Override
+    public boolean isAllowedAnyUser() throws AmetysRepositoryException
+    {
+        Node node = getNode();
         try
         {
-            getNode().setProperty(METADATA_THEMES, themes);
+            return node.hasProperty(METADATA_GRANT_ANY_USER) ? getNode().getProperty(METADATA_GRANT_ANY_USER).getBoolean() : false;
         }
         catch (RepositoryException e)
         {
-            throw new AmetysRepositoryException("Error setting the themes property.", e);
+            throw new AmetysRepositoryException("An error occurred while trying to get the 'grant-any-user' property.", e);
         }
     }
     
     @Override
-    public void removeTheme (String themeId) throws AmetysRepositoryException
+    public void setGrantAnyUser(boolean grantAnyUser) throws AmetysRepositoryException
     {
         try
         {
-            String[] themes = getThemes();
-            String[] updatedThemes = (String[]) ArrayUtils.removeElement(themes, themeId);
-            getNode().setProperty(METADATA_THEMES, updatedThemes);
+            getNode().setProperty(METADATA_GRANT_ANY_USER, grantAnyUser);
         }
         catch (RepositoryException e)
         {
-            throw new AmetysRepositoryException("Error removing theme of id " + themeId, e);
+            throw new AmetysRepositoryException("An error occurred while trying to set the 'grant-any-user' property.", e);
         }
     }
     
@@ -420,4 +450,119 @@
         }
     }
     
+    @Override
+    public void orderBefore(AmetysObject siblingObject) throws AmetysRepositoryException
+    {
+        if (siblingObject instanceof DefaultLink)
+        {
+            DefaultLink sibling = (DefaultLink) siblingObject;
+            sibling.getPath();
+            Node node = getNode();
+            String siblingPath = "";
+            try
+            {
+                siblingPath = sibling.getNode().getPath();
+                if (siblingPath.contains("/"))
+                {
+                    siblingPath = StringUtils.substringAfterLast(siblingPath, "/");
+                }
+                String path = node.getPath();
+                if (path.contains("/"))
+                {
+                    path = StringUtils.substringAfterLast(path, "/");
+                }
+                node.getParent().orderBefore(path, siblingPath);
+            }
+            catch (RepositoryException e)
+            {
+                throw new AmetysRepositoryException(String.format("Unable to order link '%s' before link '%s'", this, siblingPath), e);
+            }
+        }
+        else
+        {
+            throw new AmetysRepositoryException("A link can only be moved before another link.");
+        }
+    }
+    
+    @Override
+    public void moveTo(AmetysObject newParent, boolean renameIfExist) throws AmetysRepositoryException, RepositoryIntegrityViolationException
+    {
+        if (getParent().equals(newParent))
+        {
+            try
+            {
+                // Just order current node to the end.
+                Node node = getNode();
+                String path = node.getPath();
+                if (path.contains("/"))
+                {
+                    path = StringUtils.substringAfterLast(path, "/");
+                }
+                node.getParent().orderBefore(path, null);
+            }
+            catch (RepositoryException e)
+            {
+                throw new AmetysRepositoryException(String.format("Unable to move link '%s' outside the root link node.", this), e);
+            }
+        }
+        else
+        {
+            throw new AmetysRepositoryException("A link can only be moved before another link.");
+        }
+    }
+
+    @Override
+    public boolean canMoveTo(AmetysObject newParent) throws AmetysRepositoryException
+    {
+        return getParent().equals(newParent);
+    }
+    
+    /**
+     * Retrieves the list of values corresponding to the metadata name passed as parameter
+     * @param metadataName the name of the metadata to retrieve
+     * @return the list corresponding to the metadata name
+     */
+    private String[] _getListFromMetadataName(String metadataName)
+    {
+        try
+        {
+            if (getNode().hasProperty(metadataName))
+            {
+                Value[] values = getNode().getProperty(metadataName).getValues();
+                
+                String[] list = new String[values.length];
+                
+                for (int i = 0; i < values.length; i++)
+                {
+                    list[i] = values[i].getString();
+                }
+                
+                return list;
+            }
+            else
+            {
+                return new String[0];
+            }
+        }
+        catch (RepositoryException e)
+        {
+            throw new AmetysRepositoryException("An error occurred while trying to get the property '" + metadataName + "'.", e);
+        }
+    }
+    
+    /**
+     * Sets the list of values to the node corresponding to the metadata name passed as a parameter
+     * @param list the list of value to be set
+     */
+    private void _setListWithMetadataName(String[] list, String metadataName)
+    {
+        try
+        {
+            getNode().setProperty(metadataName, list);
+        }
+        catch (RepositoryException e)
+        {
+            throw new AmetysRepositoryException("An error occurred while trying to set the property '" + metadataName + "'.", e);
+        }
+    }
 }
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/CreateThemeAction.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/CreateThemeAction.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/CreateThemeAction.java	(working copy)
@@ -84,21 +84,22 @@
         String language = (String) jsParameters.get("language");
         Site site = _siteManager.getSite(siteName);
         
-        String label = StringUtils.defaultString((String) jsParameters.get("label"));
-        
-        // Check that the theme doesn't already exist.
-        if (themeExists(label, siteName, language))
-        {
-            result.put("error", "theme-already-exists");
-            return result;
-        }
+        String originalLabel = StringUtils.defaultString((String) jsParameters.get("label"));
         
         ModifiableTraversableAmetysObject rootNode = DirectoryHelper.getThemesNode(site, language);
         
-        DefaultTheme theme = rootNode.createChild(DefaultThemeFactory.THEME_NODE_NAME, DefaultThemeFactory.THEME_NODE_TYPE);
+        // Handle identical labels
+        String label = originalLabel;
+        int index = 2;
+        while (rootNode.hasChild(DirectoryHelper.normalizeString(label)))
+        {
+            label = originalLabel + "-" + (index++);
+        }
         
-        theme.setLabel(label);
+        DefaultTheme theme = rootNode.createChild(DirectoryHelper.normalizeString(label), DefaultThemeFactory.THEME_NODE_TYPE);
         
+        theme.setLabel(originalLabel);
+
         rootNode.saveChanges();
         
         result.put("id", theme.getId());
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/DeleteThemeAction.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/DeleteThemeAction.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/DeleteThemeAction.java	(working copy)
@@ -1,5 +1,5 @@
 /*
- *  Copyright 2012 Anyware Services
+ *  Copyright 2015 Anyware Services
  *
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -25,7 +25,6 @@
 import org.apache.cocoon.environment.Redirector;
 import org.apache.cocoon.environment.Request;
 import org.apache.cocoon.environment.SourceResolver;
-import org.apache.commons.lang.ArrayUtils;
 import org.apache.commons.lang.StringUtils;
 
 import org.ametys.cms.observation.AbstractNotifierAction;
@@ -33,13 +32,11 @@
 import org.ametys.plugins.linkdirectory.DirectoryEvents;
 import org.ametys.plugins.linkdirectory.DirectoryHelper;
 import org.ametys.plugins.linkdirectory.Theme;
-import org.ametys.plugins.linkdirectory.link.ThemeExpression;
 import org.ametys.plugins.linkdirectory.repository.DefaultLink;
 import org.ametys.plugins.linkdirectory.repository.DefaultTheme;
 import org.ametys.plugins.repository.AmetysObjectIterable;
 import org.ametys.plugins.repository.ModifiableAmetysObject;
 import org.ametys.plugins.repository.UnknownAmetysObjectException;
-import org.ametys.web.repository.page.ModifiableZoneItem;
 
 /**
  * Action which deletes a directory {@link Theme}.
@@ -71,7 +68,6 @@
                 
                 // Remove references
                 List<String> modifiedLinks = _deleteThemeRefIntoLinks(siteName, language, id);
-                _deleteThemeRefIntoServices(siteName, language, id);
                 
                 _observationManager.notify(new Event(_getCurrentUser(), DirectoryEvents.THEME_DELETED, id, new String[] {siteName, language}));
                 
@@ -94,7 +90,7 @@
     {
         List<String> modifiedLinks = new ArrayList<String>();
         
-        String xPathQuery = DirectoryHelper.getLinkQuery(siteName, lang, new ThemeExpression(themeId));
+        String xPathQuery = DirectoryHelper.getLinksQuery(siteName, lang, new ThemeExpression(themeId));
         
         AmetysObjectIterable<DefaultLink> links = _resolver.query(xPathQuery);
         
@@ -107,23 +103,4 @@
         
         return modifiedLinks;
     }
-    
-    private void _deleteThemeRefIntoServices (String siteName, String lang, String themeId)
-    {
-        String xPathQuery = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" + lang + "//element(*, ametys:zoneItem)[@ametys-internal:service = 'org.ametys.plugins.linkdirectory.DirectoryService']"; 
-        AmetysObjectIterable<ModifiableZoneItem> zoneItems = _resolver.query(xPathQuery);
-        for (ModifiableZoneItem zoneItem : zoneItems)
-        {
-            String themes = zoneItem.getServiceParameters().getString("themes", null);
-            String[] themeArr = themes != null ? StringUtils.split(themes, ',') : new String[0];
-            
-            if (ArrayUtils.indexOf(themeArr, themeId) != -1)
-            {
-                String[] updatedThemes = (String[]) ArrayUtils.removeElement(themeArr, themeId);
-                zoneItem.getServiceParameters().setMetadata("themes", StringUtils.join(updatedThemes, ","));
-                zoneItem.saveChanges();
-            }
-        }
-    }
-
 }
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/ThemeExpression.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/ThemeExpression.java	(working copy)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/ThemeExpression.java	(working copy)
@@ -13,7 +13,7 @@
  *  See the License for the specific language governing permissions and
  *  limitations under the License.
  */
-package org.ametys.plugins.linkdirectory.link;
+package org.ametys.plugins.linkdirectory.theme;
 
 import org.ametys.plugins.linkdirectory.repository.DefaultLink;
 import org.ametys.plugins.repository.query.expression.Expression;
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/ThemeGenerator.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/ThemeGenerator.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/ThemeGenerator.java	(working copy)
@@ -28,6 +28,7 @@
 import org.apache.commons.lang.StringUtils;
 import org.xml.sax.SAXException;
 
+import org.ametys.plugins.linkdirectory.DirectoryHelper;
 import org.ametys.plugins.linkdirectory.repository.DefaultTheme;
 import org.ametys.plugins.repository.AmetysObjectResolver;
 
@@ -70,8 +71,8 @@
     {
         AttributesImpl attrs = new AttributesImpl();
         attrs.addCDATAAttribute("id", theme.getId());
-        
         attrs.addCDATAAttribute("label", StringUtils.defaultString(theme.getLabel()));
+        attrs.addCDATAAttribute("name", theme.getName());
         
         XMLUtils.createElement(contentHandler, "theme", attrs);
     }
Index: main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/UpdateThemeAction.java
===================================================================
--- main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/UpdateThemeAction.java	(revision 28682)
+++ main/plugin-link-directory/src/org/ametys/plugins/linkdirectory/theme/UpdateThemeAction.java	(working copy)
@@ -15,8 +15,10 @@
  */
 package org.ametys.plugins.linkdirectory.theme;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.avalon.framework.parameters.Parameters;
@@ -27,8 +29,11 @@
 
 import org.ametys.cms.observation.Event;
 import org.ametys.plugins.linkdirectory.DirectoryEvents;
+import org.ametys.plugins.linkdirectory.DirectoryHelper;
 import org.ametys.plugins.linkdirectory.Theme;
+import org.ametys.plugins.linkdirectory.repository.DefaultLink;
 import org.ametys.plugins.linkdirectory.repository.DefaultTheme;
+import org.ametys.plugins.repository.AmetysObjectIterable;
 import org.ametys.plugins.repository.UnknownAmetysObjectException;
 
 /**
@@ -62,17 +67,39 @@
             
             theme.saveChanges();
             
+            List<String> modifiedLinks = _getReferencingLinks(siteName, language, id);
+            
             result.put("id", theme.getId());
             
             _observationManager.notify(new Event(_getCurrentUser(), DirectoryEvents.THEME_MODIFIED, theme));
+            
+            if (modifiedLinks.size() > 0)
+            {
+                result.put("modified-links", StringUtils.join(modifiedLinks, ","));
+            }
         }
         catch (UnknownAmetysObjectException e)
         {
             result.put("error", "unknown-theme");
-            getLogger().error("Unable to edit theme of id '" + id + ", because it does not not exist.", e);
+            getLogger().error("Unable to edit theme of id '" + id + ", because it does not exist.", e);
         }
         
         return result;
     }
 
+    private List<String> _getReferencingLinks (String siteName, String lang, String themeId)
+    {
+        List<String> modifiedLinks = new ArrayList<String>();
+        
+        String xPathQuery = DirectoryHelper.getLinksQuery(siteName, lang, new ThemeExpression(themeId));
+        
+        AmetysObjectIterable<DefaultLink> links = _resolver.query(xPathQuery);
+        
+        for (DefaultLink link : links)
+        {
+            modifiedLinks.add(link.getId());
+        }
+        
+        return modifiedLinks;
+    }
 }
